From 61114262cb8eba9f2b80317de0902b64caaf8774 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:28:16 -0400 Subject: [PATCH] Goodbye cli suck --- .github/CONTRIBUTING.md | 27 - .github/ISSUE_TEMPLATE/BUG_REPORT.md | 26 - .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 20 - .github/PULL_REQUEST_TEMPLATE.md | 13 - .github/workflows/codeql.yml | 74 -- .github/workflows/main.yml | 95 -- .gitignore | 17 - LICENSE | 373 ------ README.md | 35 - _constants.py | 0 install_requires.py | 2 - pip_version | 1 - pros-macos.spec | 57 - pros.icns | Bin 384035 -> 0 bytes pros.spec | 57 - pros/__init__.py | 0 pros/autocomplete/pros-complete.bash | 26 - pros/autocomplete/pros-complete.ps1 | 45 - pros/autocomplete/pros-complete.zsh | 31 - pros/autocomplete/pros.fish | 14 - pros/cli/__init__.py | 0 pros/cli/build.py | 86 -- pros/cli/click_classes.py | 166 --- pros/cli/common.py | 297 ----- pros/cli/compile_commands/intercept-cc.py | 4 - pros/cli/conductor.py | 394 ------- pros/cli/conductor_utils.py | 174 --- pros/cli/interactive.py | 45 - pros/cli/main.py | 143 --- pros/cli/misc_commands.py | 164 --- pros/cli/terminal.py | 120 -- pros/cli/test.py | 18 - pros/cli/upload.py | 208 ---- pros/cli/user_script.py | 26 - pros/cli/v5_utils.py | 325 ------ pros/common/__init__.py | 2 - pros/common/sentry.py | 142 --- pros/common/ui/__init__.py | 191 --- pros/common/ui/interactive/ConfirmModal.py | 27 - pros/common/ui/interactive/__init__.py | 0 pros/common/ui/interactive/application.py | 155 --- .../ui/interactive/components/__init__.py | 10 - .../ui/interactive/components/button.py | 24 - .../ui/interactive/components/checkbox.py | 6 - .../ui/interactive/components/component.py | 76 -- .../ui/interactive/components/container.py | 33 - .../common/ui/interactive/components/input.py | 30 - .../ui/interactive/components/input_groups.py | 14 - .../common/ui/interactive/components/label.py | 30 - pros/common/ui/interactive/observable.py | 78 -- .../ui/interactive/parameters/__init__.py | 6 - .../interactive/parameters/misc_parameters.py | 39 - .../ui/interactive/parameters/parameter.py | 27 - .../parameters/validatable_parameter.py | 60 - .../renderers/MachineOutputRenderer.py | 126 -- .../ui/interactive/renderers/Renderer.py | 28 - .../ui/interactive/renderers/__init__.py | 1 - pros/common/ui/log.py | 52 - pros/common/utils.py | 149 --- pros/conductor/__init__.py | 6 - pros/conductor/conductor.py | 405 ------- pros/conductor/depots.md | 45 - pros/conductor/depots/__init__.py | 3 - pros/conductor/depots/depot.py | 40 - pros/conductor/depots/http_depot.py | 43 - pros/conductor/depots/local_depot.py | 52 - pros/conductor/interactive/NewProjectModal.py | 73 -- .../interactive/UpdateProjectModal.py | 147 --- pros/conductor/interactive/__init__.py | 4 - pros/conductor/interactive/components.py | 41 - pros/conductor/interactive/parameters.py | 126 -- pros/conductor/project/ProjectReport.py | 25 - pros/conductor/project/ProjectTransaction.py | 174 --- pros/conductor/project/__init__.py | 428 ------- pros/conductor/project/template_resolution.py | 18 - pros/conductor/templates/__init__.py | 4 - pros/conductor/templates/base_template.py | 94 -- pros/conductor/templates/external_template.py | 27 - pros/conductor/templates/local_template.py | 24 - pros/conductor/templates/template.py | 18 - pros/conductor/transaction.py | 75 -- pros/config/__init__.py | 1 - pros/config/cli_config.py | 69 -- pros/config/config.py | 110 -- pros/ga/__init__.py | 0 pros/ga/analytics.py | 95 -- pros/jinx/__init__.py | 0 pros/jinx/server.py | 6 - pros/serial/__init__.py | 14 - pros/serial/devices/__init__.py | 2 - pros/serial/devices/generic_device.py | 13 - pros/serial/devices/stream_device.py | 48 - pros/serial/devices/system_device.py | 11 - pros/serial/devices/vex/__init__.py | 5 - pros/serial/devices/vex/comm_error.py | 7 - pros/serial/devices/vex/cortex_device.py | 154 --- pros/serial/devices/vex/crc.py | 23 - pros/serial/devices/vex/message.py | 38 - pros/serial/devices/vex/stm32_device.py | 191 --- pros/serial/devices/vex/v5_device.py | 1036 ----------------- pros/serial/devices/vex/v5_user_device.py | 49 - pros/serial/devices/vex/vex_device.py | 124 -- pros/serial/interactive/UploadProjectModal.py | 170 --- pros/serial/interactive/__init__.py | 3 - pros/serial/ports/__init__.py | 15 - pros/serial/ports/base_port.py | 40 - pros/serial/ports/direct_port.py | 73 -- pros/serial/ports/exceptions.py | 30 - pros/serial/ports/serial_share_bridge.py | 177 --- pros/serial/ports/serial_share_port.py | 83 -- pros/serial/ports/v5_wireless_port.py | 36 - pros/serial/terminal/__init__.py | 1 - pros/serial/terminal/terminal.py | 302 ----- pros/upgrade/__init__.py | 8 - pros/upgrade/instructions/__init__.py | 6 - .../upgrade/instructions/base_instructions.py | 19 - .../instructions/download_instructions.py | 34 - .../instructions/explorer_instructions.py | 18 - .../instructions/nothing_instructions.py | 9 - pros/upgrade/manifests/__init__.py | 8 - pros/upgrade/manifests/upgrade_manifest_v1.py | 47 - pros/upgrade/manifests/upgrade_manifest_v2.py | 78 -- pros/upgrade/upgrade_manager.py | 96 -- requirements.txt | 17 - setup.py | 22 - tox.ini | 6 - version | 1 - version.py | 35 - win_version | 1 - 129 files changed, 9592 deletions(-) delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md delete mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/main.yml delete mode 100644 .gitignore delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 _constants.py delete mode 100644 install_requires.py delete mode 100644 pip_version delete mode 100644 pros-macos.spec delete mode 100644 pros.icns delete mode 100644 pros.spec delete mode 100644 pros/__init__.py delete mode 100644 pros/autocomplete/pros-complete.bash delete mode 100644 pros/autocomplete/pros-complete.ps1 delete mode 100644 pros/autocomplete/pros-complete.zsh delete mode 100644 pros/autocomplete/pros.fish delete mode 100644 pros/cli/__init__.py delete mode 100644 pros/cli/build.py delete mode 100644 pros/cli/click_classes.py delete mode 100644 pros/cli/common.py delete mode 100644 pros/cli/compile_commands/intercept-cc.py delete mode 100644 pros/cli/conductor.py delete mode 100644 pros/cli/conductor_utils.py delete mode 100644 pros/cli/interactive.py delete mode 100644 pros/cli/main.py delete mode 100644 pros/cli/misc_commands.py delete mode 100644 pros/cli/terminal.py delete mode 100644 pros/cli/test.py delete mode 100644 pros/cli/upload.py delete mode 100644 pros/cli/user_script.py delete mode 100644 pros/cli/v5_utils.py delete mode 100644 pros/common/__init__.py delete mode 100644 pros/common/sentry.py delete mode 100644 pros/common/ui/__init__.py delete mode 100644 pros/common/ui/interactive/ConfirmModal.py delete mode 100644 pros/common/ui/interactive/__init__.py delete mode 100644 pros/common/ui/interactive/application.py delete mode 100644 pros/common/ui/interactive/components/__init__.py delete mode 100644 pros/common/ui/interactive/components/button.py delete mode 100644 pros/common/ui/interactive/components/checkbox.py delete mode 100644 pros/common/ui/interactive/components/component.py delete mode 100644 pros/common/ui/interactive/components/container.py delete mode 100644 pros/common/ui/interactive/components/input.py delete mode 100644 pros/common/ui/interactive/components/input_groups.py delete mode 100644 pros/common/ui/interactive/components/label.py delete mode 100644 pros/common/ui/interactive/observable.py delete mode 100644 pros/common/ui/interactive/parameters/__init__.py delete mode 100644 pros/common/ui/interactive/parameters/misc_parameters.py delete mode 100644 pros/common/ui/interactive/parameters/parameter.py delete mode 100644 pros/common/ui/interactive/parameters/validatable_parameter.py delete mode 100644 pros/common/ui/interactive/renderers/MachineOutputRenderer.py delete mode 100644 pros/common/ui/interactive/renderers/Renderer.py delete mode 100644 pros/common/ui/interactive/renderers/__init__.py delete mode 100644 pros/common/ui/log.py delete mode 100644 pros/common/utils.py delete mode 100644 pros/conductor/__init__.py delete mode 100644 pros/conductor/conductor.py delete mode 100644 pros/conductor/depots.md delete mode 100644 pros/conductor/depots/__init__.py delete mode 100644 pros/conductor/depots/depot.py delete mode 100644 pros/conductor/depots/http_depot.py delete mode 100644 pros/conductor/depots/local_depot.py delete mode 100644 pros/conductor/interactive/NewProjectModal.py delete mode 100644 pros/conductor/interactive/UpdateProjectModal.py delete mode 100644 pros/conductor/interactive/__init__.py delete mode 100644 pros/conductor/interactive/components.py delete mode 100644 pros/conductor/interactive/parameters.py delete mode 100644 pros/conductor/project/ProjectReport.py delete mode 100644 pros/conductor/project/ProjectTransaction.py delete mode 100644 pros/conductor/project/__init__.py delete mode 100644 pros/conductor/project/template_resolution.py delete mode 100644 pros/conductor/templates/__init__.py delete mode 100644 pros/conductor/templates/base_template.py delete mode 100644 pros/conductor/templates/external_template.py delete mode 100644 pros/conductor/templates/local_template.py delete mode 100644 pros/conductor/templates/template.py delete mode 100644 pros/conductor/transaction.py delete mode 100644 pros/config/__init__.py delete mode 100644 pros/config/cli_config.py delete mode 100644 pros/config/config.py delete mode 100644 pros/ga/__init__.py delete mode 100644 pros/ga/analytics.py delete mode 100644 pros/jinx/__init__.py delete mode 100644 pros/jinx/server.py delete mode 100644 pros/serial/__init__.py delete mode 100644 pros/serial/devices/__init__.py delete mode 100644 pros/serial/devices/generic_device.py delete mode 100644 pros/serial/devices/stream_device.py delete mode 100644 pros/serial/devices/system_device.py delete mode 100644 pros/serial/devices/vex/__init__.py delete mode 100644 pros/serial/devices/vex/comm_error.py delete mode 100644 pros/serial/devices/vex/cortex_device.py delete mode 100644 pros/serial/devices/vex/crc.py delete mode 100644 pros/serial/devices/vex/message.py delete mode 100644 pros/serial/devices/vex/stm32_device.py delete mode 100644 pros/serial/devices/vex/v5_device.py delete mode 100644 pros/serial/devices/vex/v5_user_device.py delete mode 100644 pros/serial/devices/vex/vex_device.py delete mode 100644 pros/serial/interactive/UploadProjectModal.py delete mode 100644 pros/serial/interactive/__init__.py delete mode 100644 pros/serial/ports/__init__.py delete mode 100644 pros/serial/ports/base_port.py delete mode 100644 pros/serial/ports/direct_port.py delete mode 100644 pros/serial/ports/exceptions.py delete mode 100644 pros/serial/ports/serial_share_bridge.py delete mode 100644 pros/serial/ports/serial_share_port.py delete mode 100644 pros/serial/ports/v5_wireless_port.py delete mode 100644 pros/serial/terminal/__init__.py delete mode 100644 pros/serial/terminal/terminal.py delete mode 100644 pros/upgrade/__init__.py delete mode 100644 pros/upgrade/instructions/__init__.py delete mode 100644 pros/upgrade/instructions/base_instructions.py delete mode 100644 pros/upgrade/instructions/download_instructions.py delete mode 100644 pros/upgrade/instructions/explorer_instructions.py delete mode 100644 pros/upgrade/instructions/nothing_instructions.py delete mode 100644 pros/upgrade/manifests/__init__.py delete mode 100644 pros/upgrade/manifests/upgrade_manifest_v1.py delete mode 100644 pros/upgrade/manifests/upgrade_manifest_v2.py delete mode 100644 pros/upgrade/upgrade_manager.py delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 tox.ini delete mode 100644 version delete mode 100644 version.py delete mode 100644 win_version diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 69543448..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing to PROS - -:tada: :+1: :steam_locomotive: Thanks for taking the time to contribute! :steam_locomotive: :+1: :tada: - -**Did you find a bug?** -- **Before opening an issue, make sure you are in the right repository!** - purduesigbots maintains four repositories related to PROS: - - [purduesigbots/pros](https://github.com/purduesigbots/pros): the repository containing the source code for the kernel the user-facing API. Issues should be opened here if they affect the code you write (e.g., "I would like to be able to do X with PROS," or "when I call X doesn't work as I expect") - - [purduesigbots/pros-cli](https://github.com/purduesigbots/pros-cli): the repository containing the source code for the command line interface (CLI). Issues should be opened here if they concern the PROS CLI (e.g., problems with commands like `pros make`), as well as project creation and management. - - [purduesigbots/pros-atom](https://github.com/purduesigbots/pros-atom): the repository containing the source code for the Atom package. Issues should be opened here if they concern the coding experience within Atom or the PROS Editor (e.g., "there is no button to do X," or "the linter is spamming my interface with errors"). - - [purduesigbots/pros-docs](https://github.com/purduesigbots/pros-docs): the repository containing the source code for [our documentation website](https://pros.cs.purdue.edu). Issues should be opened here if they concern available documentation (e.g., "there is not guide on using ," or "the documentation says to do X, but only Y works") -- Ensure the bug wasn't already reported by searching GitHub [issues](https://github.com/purduesigbots/pros-cli/issues) -- If you're unable to find an issue, [open](https://github.com/purduesigbots/pros-cli/issues/new) a new one. - -**Did you patch a bug or add a new feature?** -1. [Fork](https://github.com/purduesigbots/pros-cli/fork) and clone the repository -2. Create a new branch: `git checkout -b my-branch-name` -3. Make your changes -4. Push to your fork and submit a pull request. -5. Wait for your pull request to be reviewed. In order to ensure that the PROS CLI user experience is as smooth as possible, we take extra time to test pull requests. As a result, your pull request may take some time to be merged into master. - -Here are a few tips that can help expedite your pull request being accepted: -- Follow existing code's style-- we use the `flake8` linter. -- Document why you made the changes you did. -- Keep your change as focused as possible. If you have multiple independent changes, make a pull request for each. -- If you did some testing, describe your procedure and results. -- If you're fixing an issue, reference it by number. diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md deleted file mode 100644 index 940bbe67..00000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Bug Report template -about: Default template for bug reports -title: "🐛" -labels: '' -assignees: '' ---- - -#### Expected Behavior: - - -#### Actual Behavior: - - -#### Steps to reproduce: - - -#### System information: -Operating System: - -PROS Version: - -#### Additional Information - - -#### Screenshots/Output Dumps/Stack Traces diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md deleted file mode 100644 index 07e209ad..00000000 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature Request template -about: Default template for feature requests -title: "✨" -labels: '' -assignees: '' - ---- - -#### Requested Feature - - -#### Is this Feature Related to a Problem? - - -#### Benefits of Feature - - -#### Additional Information - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 6abc1153..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -#### Summary: - - -#### Motivation: - - -##### References (optional): - - -#### Test Plan: - - -- [ ] test item diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index a903ec32..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "develop", master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "develop" ] - schedule: - - cron: '16 12 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index c4f997ac..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Build PROS CLI - -on: - push: - pull_request: - -jobs: - update_build_number: - runs-on: ubuntu-latest - outputs: - output1: ${{ steps.step1.outputs.test }} - steps: - - uses: actions/checkout@v3.1.0 - with: - fetch-depth: 0 - - name: Update Build Number - id: step1 - run: | - git describe --tags - git clean -f - python3 version.py - echo "::set-output name=test::$(cat version)" - - build: - needs: update_build_number - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - uses: actions/checkout@v3.1.0 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v4.3.0 - with: - python-version: 3.9 - cache: 'pip' - if: matrix.os != 'macos-latest' - - - name: Setup Python MacOS - run: | - wget https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg - sudo installer -verbose -pkg ./python-3.10.11-macos11.pkg -target / - echo "/Library/Frameworks/Python.framework/Versions/3.10/bin" >> $GITHUB_PATH - if: matrix.os == 'macos-latest' - - - name: Install Requirements - run: python3 -m pip install --upgrade pip && pip3 install wheel && pip3 install -r requirements.txt && pip3 uninstall -y typing - - - name: Build Wheel - run: python3 setup.py bdist_wheel - if: matrix.os == 'ubuntu-latest' - - - name: Upload Wheel - uses: actions/upload-artifact@v3.1.0 - with: - name: pros-cli-wheel-${{needs.update_build_number.outputs.output1}} - path: dist/* - if: matrix.os == 'ubuntu-latest' - - - name: Run Pyinstaller - run: | - python3 version.py - pyinstaller pros.spec - pyinstaller --onefile pros/cli/compile_commands/intercept-cc.py --name=intercept-cc - pyinstaller --onefile pros/cli/compile_commands/intercept-cc.py --name=intercept-c++ - if: matrix.os != 'macos-latest' - - - name: Run Pyinstaller MacOS - run: | - pip3 uninstall -y charset_normalizer - git clone https://github.com/Ousret/charset_normalizer.git - pip3 install -e ./charset_normalizer - python3 version.py - pyinstaller pros-macos.spec - pyinstaller --onefile pros/cli/compile_commands/intercept-cc.py --name=intercept-cc --target-arch=universal2 - pyinstaller --onefile pros/cli/compile_commands/intercept-cc.py --name=intercept-c++ --target-arch=universal2 - if: matrix.os == 'macos-latest' - - - name: Package Everything Up - shell: bash - run: | - cd dist/ - mv intercept-cc pros - mv intercept-c++ pros - - - name: Upload Artifact - uses: actions/upload-artifact@v3.1.0 - with: - name: ${{ matrix.os }}-${{needs.update_build_number.outputs.output1}} - path: dist/pros/* diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6fac33e7..00000000 --- a/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -.idea -.pyc -__pycache__/ - -build/ -.cache/ -dist/ - -*.egg-info/ - -out/ -*.zip -*_pros_capture.png - -venv/ - -*.pyc diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a612ad98..00000000 --- a/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md deleted file mode 100644 index 726086b3..00000000 --- a/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# PROS CLI - -[![Build Status](https://dev.azure.com/purdue-acm-sigbots/CLI/_apis/build/status/purduesigbots.pros-cli?branchName=develop)](https://dev.azure.com/purdue-acm-sigbots/CLI/_build/latest?definitionId=6&branchName=develop) - -PROS is the only open source development environment for the VEX EDR Platform. - -This project provides all of the project management related tasks for PROS. It is currently responsible for: - - Downloading kernel templates - - Creating, upgrading projects - - Uploading binaries to VEX Microcontrollers - -This project is built in Python 3.6, and executables are built on cx_Freeze. - -## Installing for development -PROS CLI can be installed directly from source with the following prerequisites: - - Python 3.5 - - PIP (default in Python 3.6) - - Setuptools (default in Python 3.6) - -Clone this repository, then run `pip install -e `. Pip will install all the dependencies necessary. - -## About this project -Here's a quick breakdown of the packages involved in this project: - -- `pros.cli`: responsible for parsing arguments and running requested command -- `pros.common.ui`: provides user interface functions used throughout the PROS CLI (such as logging facilities, machine-readable output) -- `pros.conductor`: provides all project management related tasks - - `pros.conductor.depots`: logic for downloading templates - - `pros.conductor.templates`: logic for maintaining information about a template -- `pros.config`: provides base classes for configuration files in PROS (and also the global cli.pros config file) -- `pros.jinx`: JINX parsing and server -- `pros.serial`: package for all serial communication with VEX Microcontrollers -- `pros.upgrade`: package for upgrading the PROS CLI, including downloading and executing installation sequence - -See https://pros.cs.purdue.edu/v5/cli for end user documentation and developer notes. diff --git a/_constants.py b/_constants.py deleted file mode 100644 index e69de29b..00000000 diff --git a/install_requires.py b/install_requires.py deleted file mode 100644 index 6aad2a80..00000000 --- a/install_requires.py +++ /dev/null @@ -1,2 +0,0 @@ -with open('requirements.txt') as reqs: - install_requires = [req.strip() for req in reqs.readlines()] diff --git a/pip_version b/pip_version deleted file mode 100644 index e5b8a844..00000000 --- a/pip_version +++ /dev/null @@ -1 +0,0 @@ -3.5.4 \ No newline at end of file diff --git a/pros-macos.spec b/pros-macos.spec deleted file mode 100644 index 0ff36d3f..00000000 --- a/pros-macos.spec +++ /dev/null @@ -1,57 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -# Write version info into _constants.py resource file - -from distutils.util import get_platform - -with open('_constants.py', 'w') as f: - f.write("CLI_VERSION = \"{}\"\n".format(open('version').read().strip())) - f.write("FROZEN_PLATFORM_V1 = \"{}\"\n".format("Windows64" if get_platform() == "win-amd64" else "Windows86")) - -block_cipher = None - - -a = Analysis( - ['pros/cli/main.py'], - pathex=[], - binaries=[], - datas=[('pros/autocomplete/*', 'pros/autocomplete')], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='pros', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch='universal2', - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='pros', -) diff --git a/pros.icns b/pros.icns deleted file mode 100644 index 76d5fcfdb552196bbe53c9b011da2bdfcc608dc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 384035 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 diff --git a/pros.spec b/pros.spec deleted file mode 100644 index 9910e946..00000000 --- a/pros.spec +++ /dev/null @@ -1,57 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -# Write version info into _constants.py resource file - -from distutils.util import get_platform - -with open('_constants.py', 'w') as f: - f.write("CLI_VERSION = \"{}\"\n".format(open('version').read().strip())) - f.write("FROZEN_PLATFORM_V1 = \"{}\"\n".format("Windows64" if get_platform() == "win-amd64" else "Windows86")) - -block_cipher = None - - -a = Analysis( - ['pros/cli/main.py'], - pathex=[], - binaries=[], - datas=[('pros/autocomplete/*', 'pros/autocomplete')], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='pros', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='pros', -) diff --git a/pros/__init__.py b/pros/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pros/autocomplete/pros-complete.bash b/pros/autocomplete/pros-complete.bash deleted file mode 100644 index 4c466464..00000000 --- a/pros/autocomplete/pros-complete.bash +++ /dev/null @@ -1,26 +0,0 @@ -_pros_completion() { - local IFS=$'\n' - local response - response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _PROS_COMPLETE=bash_complete $1) - for completion in $response; do - IFS=',' read type value <<<"$completion" - if [[ $type == 'dir' ]]; then - COMPREPLY=() - compopt -o dirnames - elif [[ $type == 'file' ]]; then - COMPREPLY=() - compopt -o default - elif [[ $type == 'plain' ]]; then - COMPREPLY+=($value) - fi - done - return 0 -} -_pros_completion_setup() { - if [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then - complete -o nosort -F _pros_completion pros - else - complete -F _pros_completion pros - fi -} -_pros_completion_setup diff --git a/pros/autocomplete/pros-complete.ps1 b/pros/autocomplete/pros-complete.ps1 deleted file mode 100644 index 319aba26..00000000 --- a/pros/autocomplete/pros-complete.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -# Modified from https://github.com/StephLin/click-pwsh/blob/main/click_pwsh/shell_completion.py#L11 -Register-ArgumentCompleter -Native -CommandName pros -ScriptBlock { - param($wordToComplete, $commandAst, $cursorPosition) - $env:COMP_WORDS = $commandAst - $env:COMP_WORDS = $env:COMP_WORDS.replace('\\', '/') - $incompleteCommand = $commandAst.ToString() - $myCursorPosition = $cursorPosition - if ($myCursorPosition -gt $incompleteCommand.Length) { - $myCursorPosition = $incompleteCommand.Length - } - $env:COMP_CWORD = @($incompleteCommand.substring(0, $myCursorPosition).Split(" ") | Where-Object { $_ -ne "" }).Length - if ( $wordToComplete.Length -gt 0) { $env:COMP_CWORD -= 1 } - $env:_PROS_COMPLETE = "powershell_complete" - pros | ForEach-Object { - $type, $value, $help = $_.Split(",", 3) - if ( ($type -eq "plain") -and ![string]::IsNullOrEmpty($value) ) { - [System.Management.Automation.CompletionResult]::new($value, $value, "ParameterValue", $value) - } - elseif ( ($type -eq "file") -or ($type -eq "dir") ) { - if ([string]::IsNullOrEmpty($wordToComplete)) { - $dir = "./" - } - else { - $dir = $wordToComplete.replace('\\', '/') - } - if ( (Test-Path -Path $dir) -and ((Get-Item $dir) -is [System.IO.DirectoryInfo]) ) { - [System.Management.Automation.CompletionResult]::new($dir, $dir, "ParameterValue", $dir) - } - Get-ChildItem -Path $dir | Resolve-Path -Relative | ForEach-Object { - $path = $_.ToString().replace('\\', '/').replace('Microsoft.PowerShell.Core/FileSystem::', '') - $isDir = $false - if ((Get-Item $path) -is [System.IO.DirectoryInfo]) { - $path = $path + "/" - $isDir = $true - } - if ( ($type -eq "file") -or ( ($type -eq "dir") -and $isDir ) ) { - [System.Management.Automation.CompletionResult]::new($path, $path, "ParameterValue", $path) - } - } - } - } - $env:COMP_WORDS = $null | Out-Null - $env:COMP_CWORD = $null | Out-Null - $env:_PROS_COMPLETE = $null | Out-Null -} diff --git a/pros/autocomplete/pros-complete.zsh b/pros/autocomplete/pros-complete.zsh deleted file mode 100644 index 754a0bef..00000000 --- a/pros/autocomplete/pros-complete.zsh +++ /dev/null @@ -1,31 +0,0 @@ -_pros_completion() { - local -a completions - local -a completions_with_descriptions - local -a response - (( ! $+commands[pros] )) && return 1 - response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _PROS_COMPLETE=zsh_complete pros)}") - for type key descr in ${response}; do - if [[ "$type" == "plain" ]]; then - if [[ "$descr" == "_" ]]; then - completions+=("$key") - else - completions_with_descriptions+=("$key":"$descr") - fi - elif [[ "$type" == "dir" ]]; then - _path_files -/ - elif [[ "$type" == "file" ]]; then - _path_files -f - fi - done - if [ -n "$completions_with_descriptions" ]; then - _describe -V unsorted completions_with_descriptions -U - fi - if [ -n "$completions" ]; then - compadd -U -V unsorted -a completions - fi -} -if [[ $zsh_eval_context[-1] == loadautofunc ]]; then - _pros_completion "$@" -else - compdef _pros_completion pros -fi \ No newline at end of file diff --git a/pros/autocomplete/pros.fish b/pros/autocomplete/pros.fish deleted file mode 100644 index 73fc051c..00000000 --- a/pros/autocomplete/pros.fish +++ /dev/null @@ -1,14 +0,0 @@ -function _pros_completion; - set -l response (env _PROS_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) pros); - for completion in $response; - set -l metadata (string split "," $completion); - if test $metadata[1] = "dir"; - __fish_complete_directories $metadata[2]; - else if test $metadata[1] = "file"; - __fish_complete_path $metadata[2]; - else if test $metadata[1] = "plain"; - echo $metadata[2]; - end; - end; -end; -complete --no-files --command pros --arguments "(_pros_completion)"; \ No newline at end of file diff --git a/pros/cli/__init__.py b/pros/cli/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pros/cli/build.py b/pros/cli/build.py deleted file mode 100644 index 9ed2a742..00000000 --- a/pros/cli/build.py +++ /dev/null @@ -1,86 +0,0 @@ -import ctypes -import sys -from typing import * - -import click - -import pros.conductor as c -from pros.ga.analytics import analytics -from pros.cli.common import default_options, logger, project_option, pros_root, shadow_command -from .upload import upload - - -@pros_root -def build_cli(): - pass - - -@build_cli.command(aliases=['build','m']) -@project_option() -@click.argument('build-args', nargs=-1) -@default_options -def make(project: c.Project, build_args): - """ - Build current PROS project or cwd - """ - analytics.send("make") - exit_code = project.compile(build_args) - if exit_code != 0: - if sys.platform == 'win32': - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) - - logger(__name__).error(f'Failed to make project: Exit Code {exit_code}', extra={'sentry': False}) - raise click.ClickException('Failed to build') - return exit_code - - -@build_cli.command('make-upload', aliases=['mu'], hidden=True) -@click.option('build_args', '--make', '-m', multiple=True, help='Send arguments to make (e.g. compile target)') -@shadow_command(upload) -@project_option() -@click.pass_context -def make_upload(ctx, project: c.Project, build_args: List[str], **upload_args): - analytics.send("make-upload") - ctx.invoke(make, project=project, build_args=build_args) - ctx.invoke(upload, project=project, **upload_args) - - -@build_cli.command('make-upload-terminal', aliases=['mut'], hidden=True) -@click.option('build_args', '--make', '-m', multiple=True, help='Send arguments to make (e.g. compile target)') -@shadow_command(upload) -@project_option() -@click.pass_context -def make_upload_terminal(ctx, project: c.Project, build_args, **upload_args): - analytics.send("make-upload-terminal") - from .terminal import terminal - ctx.invoke(make, project=project, build_args=build_args) - ctx.invoke(upload, project=project, **upload_args) - ctx.invoke(terminal, port=project.target, request_banner=False) - - -@build_cli.command('build-compile-commands', hidden=True) -@project_option() -@click.option('--suppress-output/--show-output', 'suppress_output', default=False, show_default=True, - help='Suppress output') -@click.option('--compile-commands', type=click.File('w'), default=None) -@click.option('--sandbox', default=False, is_flag=True) -@click.argument('build-args', nargs=-1) -@default_options -def build_compile_commands(project: c.Project, suppress_output: bool, compile_commands, sandbox: bool, - build_args: List[str]): - """ - Build a compile_commands.json compatible with cquery - :return: - """ - analytics.send("build-compile-commands") - exit_code = project.make_scan_build(build_args, cdb_file=compile_commands, suppress_output=suppress_output, - sandbox=sandbox) - if exit_code != 0: - if sys.platform == 'win32': - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) - - logger(__name__).error(f'Failed to make project: Exit Code {exit_code}', extra={'sentry': False}) - raise click.ClickException('Failed to build') - return exit_code diff --git a/pros/cli/click_classes.py b/pros/cli/click_classes.py deleted file mode 100644 index b071c938..00000000 --- a/pros/cli/click_classes.py +++ /dev/null @@ -1,166 +0,0 @@ -from collections import defaultdict -from typing import * - -from rich_click import RichCommand -import click.decorators -from click import ClickException -from pros.conductor.project import Project as p -from pros.common.utils import get_version - - -class PROSFormatted(RichCommand): - """ - Common format functions used in the PROS derived classes. Derived classes mix and match which functions are needed - """ - - def __init__(self, *args, hidden: bool = False, **kwargs): - super(PROSFormatted, self).__init__(*args, **kwargs) - self.hidden = hidden - - def format_commands(self, ctx, formatter): - """Extra format methods for multi methods that adds all the commands - after the options. - """ - if not hasattr(self, 'list_commands'): - return - rows = [] - for subcommand in self.list_commands(ctx): - cmd = self.get_command(ctx, subcommand) - # What is this, the tool lied about a command. Ignore it - if cmd is None: - continue - if hasattr(cmd, 'hidden') and cmd.hidden: - continue - - help = cmd.short_help or '' - rows.append((subcommand, help)) - - if rows: - with formatter.section('Commands'): - formatter.write_dl(rows) - - def format_options(self, ctx, formatter): - """Writes all the options into the formatter if they exist.""" - opts: DefaultDict[str, List] = defaultdict(lambda: []) - for param in self.get_params(ctx): - rv = param.get_help_record(ctx) - if rv is not None: - if hasattr(param, 'group'): - opts[param.group].append(rv) - else: - opts['Options'].append(rv) - - if len(opts['Options']) > 0: - with formatter.section('Options'): - formatter.write_dl(opts['Options']) - opts.pop('Options') - - for group, options in opts.items(): - with formatter.section(group): - formatter.write_dl(options) - - self.format_commands(ctx, formatter) - -class PROSCommand(PROSFormatted, click.Command): - pass - - -class PROSMultiCommand(PROSFormatted, click.MultiCommand): - def get_command(self, ctx, cmd_name): - super().get_command(ctx, cmd_name) - - -class PROSOption(click.Option): - def __init__(self, *args, hidden: bool = False, group: str = None, **kwargs): - super().__init__(*args, **kwargs) - self.hidden = hidden - self.group = group - - def get_help_record(self, ctx): - if hasattr(self, 'hidden') and self.hidden: - return - return super().get_help_record(ctx) - -class PROSDeprecated(click.Option): - def __init__(self, *args, replacement: str = None, **kwargs): - kwargs['help'] = "This option has been deprecated." - if not replacement==None: - kwargs['help'] += " Its replacement is '--{}'".format(replacement) - super(PROSDeprecated, self).__init__(*args, **kwargs) - self.group = "Deprecated" - self.optiontype = "flag" if str(self.type)=="BOOL" else "switch" - self.to_use = replacement - self.arg = args[0][len(args[0])-1] - self.msg = "The '{}' {} has been deprecated. Please use '--{}' instead." - if replacement==None: - self.msg = self.msg.split(".")[0]+"." - - def type_cast_value(self, ctx, value): - if not value==self.default: - print("Warning! : "+self.msg.format(self.arg, self.optiontype, self.to_use)+"\n") - return value - -class PROSGroup(PROSFormatted, click.Group): - def __init__(self, *args, **kwargs): - super(PROSGroup, self).__init__(*args, **kwargs) - self.cmd_dict = dict() - - def command(self, *args, aliases=None, **kwargs): - aliases = aliases or [] - - def decorator(f): - for alias in aliases: - self.cmd_dict[alias] = f.__name__ if len(args) == 0 else args[0] - - cmd = super(PROSGroup, self).command(*args, cls=kwargs.pop('cls', PROSCommand), **kwargs)(f) - self.add_command(cmd) - return cmd - - return decorator - - def group(self, aliases=None, *args, **kwargs): - aliases = aliases or [] - - def decorator(f): - for alias in aliases: - self.cmd_dict[alias] = f.__name__ - cmd = super(PROSGroup, self).group(*args, cls=kwargs.pop('cls', PROSGroup), **kwargs)(f) - self.add_command(cmd) - return cmd - - return decorator - - def get_command(self, ctx, cmd_name): - # return super(PROSGroup, self).get_command(ctx, cmd_name) - suggestion = super(PROSGroup, self).get_command(ctx, cmd_name) - if suggestion is not None: - return suggestion - if cmd_name in self.cmd_dict: - return super(PROSGroup, self).get_command(ctx, self.cmd_dict[cmd_name]) - - # fall back to guessing - matches = {x for x in self.list_commands(ctx) if x.startswith(cmd_name)} - matches.union({x for x in self.cmd_dict.keys() if x.startswith(cmd_name)}) - if len(matches) == 1: - return super(PROSGroup, self).get_command(ctx, matches.pop()) - return None - - -class PROSRoot(PROSGroup): - pass - - -class PROSCommandCollection(PROSFormatted, click.CommandCollection): - def invoke(self, *args, **kwargs): - # should change none of the behavior of invoke / ClientException - # should just sit in the pipeline and do a quick echo before - # letting everything else go through. - try: - super(PROSCommandCollection, self).invoke(*args, **kwargs) - except ClickException as e: - click.echo("PROS-CLI Version: {}".format(get_version())) - isProject = p.find_project("") - if (isProject): #check if there is a project - curr_proj = p() - click.echo("PROS-Kernel Version: {}".format(curr_proj.kernel)) - raise e \ No newline at end of file diff --git a/pros/cli/common.py b/pros/cli/common.py deleted file mode 100644 index 6c12fa06..00000000 --- a/pros/cli/common.py +++ /dev/null @@ -1,297 +0,0 @@ -import click.core - -from pros.common.sentry import add_tag -from pros.ga.analytics import analytics -from pros.common.utils import * -from pros.common.ui import echo -from .click_classes import * - - -def verbose_option(f: Union[click.Command, Callable]): - def callback(ctx: click.Context, param: click.core.Parameter, value: Any): - if value is None: - return None - ctx.ensure_object(dict) - if isinstance(value, str): - value = getattr(logging, value.upper(), None) - if not isinstance(value, int): - raise ValueError('Invalid log level: {}'.format(value)) - if value: - logger().setLevel(min(logger().level, logging.INFO)) - stdout_handler = ctx.obj['click_handler'] # type: logging.Handler - stdout_handler.setLevel(logging.INFO) - logger(__name__).info('Verbose messages enabled') - return value - - return click.option('--verbose', help='Enable verbose output', is_flag=True, is_eager=True, expose_value=False, - callback=callback, cls=PROSOption, group='Standard Options')(f) - - -def debug_option(f: Union[click.Command, Callable]): - def callback(ctx: click.Context, param: click.core.Parameter, value: Any): - if value is None: - return None - ctx.ensure_object(dict) - if isinstance(value, str): - value = getattr(logging, value.upper(), None) - if not isinstance(value, int): - raise ValueError('Invalid log level: {}'.format(value)) - if value: - logging.getLogger().setLevel(logging.DEBUG) - stdout_handler = ctx.obj['click_handler'] # type: logging.Handler - stdout_handler.setLevel(logging.DEBUG) - logging.getLogger(__name__).info('Debugging messages enabled') - if logger('pros').isEnabledFor(logging.DEBUG): - logger('pros').debug(f'CLI Version: {get_version()}') - return value - - return click.option('--debug', help='Enable debugging output', is_flag=True, is_eager=True, expose_value=False, - callback=callback, cls=PROSOption, group='Standard Options')(f) - - -def logging_option(f: Union[click.Command, Callable]): - def callback(ctx: click.Context, param: click.core.Parameter, value: Any): - if value is None: - return None - ctx.ensure_object(dict) - if isinstance(value, str): - value = getattr(logging, value.upper(), None) - if not isinstance(value, int): - raise ValueError('Invalid log level: {}'.format(value)) - logging.getLogger().setLevel(min(logger().level, value)) - stdout_handler = ctx.obj['click_handler'] # type: logging.Handler - stdout_handler.setLevel(value) - return value - - return click.option('-l', '--log', help='Logging level', is_eager=True, expose_value=False, callback=callback, - type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']), - cls=PROSOption, group='Standard Options')(f) - - -def logfile_option(f: Union[click.Command, Callable]): - def callback(ctx: click.Context, param: click.core.Parameter, value: Any): - if value is None or value[0] is None: - return None - ctx.ensure_object(dict) - level = None - if isinstance(value[1], str): - level = getattr(logging, value[1].upper(), None) - if not isinstance(level, int): - raise ValueError('Invalid log level: {}'.format(value[1])) - handler = logging.FileHandler(value[0], mode='w') - fmt_str = '%(name)s.%(funcName)s:%(levelname)s - %(asctime)s - %(message)s' - handler.setFormatter(logging.Formatter(fmt_str)) - handler.setLevel(level) - logging.getLogger().addHandler(handler) - stdout_handler = ctx.obj['click_handler'] # type: logging.Handler - stdout_handler.setLevel(logging.getLogger().level) # pin stdout_handler to its current log level - logging.getLogger().setLevel(min(logging.getLogger().level, level)) - - return click.option('--logfile', help='Log messages to a file', is_eager=True, expose_value=False, - callback=callback, default=(None, None), - type=click.Tuple( - [click.Path(), click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])] - ), cls=PROSOption, group='Standard Options')(f) - - -def machine_output_option(f: Union[click.Command, Callable]): - """ - provides a wrapper for creating the machine output option (so don't have to create callback, parameters, etc.) - """ - - def callback(ctx: click.Context, param: click.Parameter, value: str): - ctx.ensure_object(dict) - add_tag('machine-output', value) # goes in sentry report - if value: - ctx.obj[param.name] = value - logging.getLogger().setLevel(logging.DEBUG) - stdout_handler = ctx.obj['click_handler'] # type: logging.Handler - stdout_handler.setLevel(logging.DEBUG) - logging.getLogger(__name__).info('Debugging messages enabled') - return value - - decorator = click.option('--machine-output', expose_value=False, is_flag=True, default=False, is_eager=True, - help='Enable machine friendly output.', callback=callback, cls=PROSOption, hidden=True)(f) - decorator.__name__ = f.__name__ - return decorator - -def no_sentry_option(f: Union[click.Command, Callable]): - """ - disables the sentry y/N prompt when an error/exception occurs - """ - def callback(ctx: click.Context, param: click.Parameter, value: bool): - ctx.ensure_object(dict) - add_tag('no-sentry',value) - if value: - pros.common.sentry.disable_prompt() - decorator = click.option('--no-sentry', expose_value=False, is_flag=True, default=True, is_eager=True, - help="Disable sentry reporting prompt.", callback=callback, cls=PROSOption, hidden=True)(f) - decorator.__name__ = f.__name__ - return decorator - -def no_analytics(f: Union[click.Command, Callable]): - """ - Don't use analytics for this command - """ - def callback(ctx: click.Context, param: click.Parameter, value: bool): - ctx.ensure_object(dict) - add_tag('no-analytics',value) - if value: - echo("Not sending analytics for this command.\n") - analytics.useAnalytics = False - pass - decorator = click.option('--no-analytics', expose_value=False, is_flag=True, default=False, is_eager=True, - help="Don't send analytics for this command.", callback=callback, cls=PROSOption, hidden=True)(f) - decorator.__name__ = f.__name__ - return decorator - -def default_options(f: Union[click.Command, Callable]): - """ - combines verbosity, debug, machine output, no analytics, and no sentry options - """ - decorator = debug_option(verbose_option(logging_option(logfile_option(machine_output_option(no_sentry_option(no_analytics(f))))))) - decorator.__name__ = f.__name__ - return decorator - - -def template_query(arg_name='query', required: bool = False): - """ - provides a wrapper for conductor commands which require an optional query - - Ignore unknown options is required in context_settings for the command: - context_settings={'ignore_unknown_options': True} - """ - - def callback(ctx: click.Context, param: click.Parameter, value: Tuple[str, ...]): - import pros.conductor as c - value = list(value) - spec = None - if len(value) > 0 and not value[0].startswith('--'): - spec = value.pop(0) - if not spec and required: - raise ValueError(f'A {arg_name} is required to perform this command') - query = c.BaseTemplate.create_query(spec, - **{value[i][2:]: value[i + 1] for i in - range(0, int(len(value) / 2) * 2, 2)}) - logger(__name__).debug(query) - return query - - def wrapper(f: Union[click.Command, Callable]): - return click.argument(arg_name, nargs=-1, required=required, callback=callback)(f) - - return wrapper - - -def project_option(arg_name='project', required: bool = True, default: str = '.', allow_none: bool = False): - def callback(ctx: click.Context, param: click.Parameter, value: str): - if allow_none and value is None: - return None - import pros.conductor as c - project_path = c.Project.find_project(value) - if project_path is None: - if allow_none: - return None - elif required: - raise click.UsageError(f'{os.path.abspath(value or ".")} is not inside a PROS project. ' - f'Execute this command from within a PROS project or specify it ' - f'with --project project/path') - else: - return None - - return c.Project(project_path) - - def wrapper(f: Union[click.Command, Callable]): - return click.option(f'--{arg_name}', callback=callback, required=required, - default=default, type=click.Path(exists=True), show_default=True, - help='PROS Project directory or file')(f) - - return wrapper - - -def shadow_command(command: click.Command): - def wrapper(f: Union[click.Command, Callable]): - if isinstance(f, click.Command): - f.params.extend(p for p in command.params if p.name not in [p.name for p in command.params]) - else: - if not hasattr(f, '__click_params__'): - f.__click_params__ = [] - f.__click_params__.extend(p for p in command.params if p.name not in [p.name for p in f.__click_params__]) - return f - - return wrapper - - -root_commands = [] - - -def pros_root(f): - decorator = click.group(cls=PROSRoot)(f) - decorator.__name__ = f.__name__ - root_commands.append(decorator) - return decorator - - -def resolve_v5_port(port: Optional[str], type: str, quiet: bool = False) -> Tuple[Optional[str], bool]: - """ - Detect serial ports that can be used to interact with a V5. - - Returns a tuple of (port?, is_joystick). port will be None if no ports are - found, and is_joystick is False unless type == 'user' and the port is - determined to be a controller. This is useful in e.g. - pros.cli.terminal:terminal where the communication protocol is different for - wireless interaction. - """ - from pros.serial.devices.vex import find_v5_ports - # If a port is specified manually, we'll just assume it's - # not a joystick. - is_joystick = False - if not port: - ports = find_v5_ports(type) - logger(__name__).debug('Ports: {}'.format(';'.join([str(p.__dict__) for p in ports]))) - if len(ports) == 0: - if not quiet: - logger(__name__).error('No {0} ports were found! If you think you have a {0} plugged in, ' - 'run this command again with the --debug flag'.format('v5'), - extra={'sentry': False}) - return None, False - if len(ports) > 1: - if not quiet: - brain_id = click.prompt('Multiple {} Brains were found. Please choose one to upload the program: [{}]' - .format('v5', ' | '.join([p.product.split(' ')[-1] for p in ports])), - default=ports[0].product.split(' ')[-1], - show_default=False, - type=click.Choice([p.description.split(' ')[-1] for p in ports])) - port = [p.device for p in ports if p.description.split(' ')[-1] == brain_id][0] - - assert port in [p.device for p in ports] - else: - return None, False - else: - port = ports[0].device - is_joystick = type == 'user' and 'Controller' in ports[0].description - logger(__name__).info('Automatically selected {}'.format(port)) - return port, is_joystick - - -def resolve_cortex_port(port: Optional[str], quiet: bool = False) -> Optional[str]: - from pros.serial.devices.vex import find_cortex_ports - if not port: - ports = find_cortex_ports() - if len(ports) == 0: - if not quiet: - logger(__name__).error('No {0} ports were found! If you think you have a {0} plugged in, ' - 'run this command again with the --debug flag'.format('cortex'), - extra={'sentry': False}) - return None - if len(ports) > 1: - if not quiet: - port = click.prompt('Multiple {} ports were found. Please choose one: '.format('cortex'), - default=ports[0].device, - type=click.Choice([p.device for p in ports])) - assert port in [p.device for p in ports] - else: - return None - else: - port = ports[0].device - logger(__name__).info('Automatically selected {}'.format(port)) - return port diff --git a/pros/cli/compile_commands/intercept-cc.py b/pros/cli/compile_commands/intercept-cc.py deleted file mode 100644 index 66026e54..00000000 --- a/pros/cli/compile_commands/intercept-cc.py +++ /dev/null @@ -1,4 +0,0 @@ -from libscanbuild.intercept import intercept_compiler_wrapper - -if __name__ == '__main__': - intercept_compiler_wrapper() diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py deleted file mode 100644 index 38d43235..00000000 --- a/pros/cli/conductor.py +++ /dev/null @@ -1,394 +0,0 @@ -import os.path -from itertools import groupby - -import pros.common.ui as ui -import pros.conductor as c -from pros.cli.common import * -from pros.conductor.templates import ExternalTemplate -from pros.ga.analytics import analytics - - -@pros_root -def conductor_cli(): - pass - - -@conductor_cli.group(cls=PROSGroup, aliases=['cond', 'c', 'conduct'], short_help='Perform project management for PROS') -@default_options -def conductor(): - """ - Conductor is PROS's project management facility. It is responsible for obtaining - templates for which to create projects from. - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - pass - - -@conductor.command(aliases=['download'], short_help='Fetch/Download a remote template', - context_settings={'ignore_unknown_options': True}) -@template_query(required=True) -@default_options -def fetch(query: c.BaseTemplate): - """ - Fetch/download a template from a depot. - - Only a template spec is required. A template spec is the name and version - of the template formatted as name@version (libblrs@1.0.0). Semantic version - ranges are accepted (e.g., libblrs@^1.0.0). The version parameter is also - optional (e.g., libblrs) - - Additional parameters are available according to the depot. - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("fetch-template") - template_file = None - if os.path.exists(query.identifier): - template_file = query.identifier - elif os.path.exists(query.name) and query.version is None: - template_file = query.name - elif query.metadata.get('origin', None) == 'local': - if 'location' not in query.metadata: - logger(__name__).error('--location option is required for the local depot. Specify --location ') - logger(__name__).debug(f'Query options provided: {query.metadata}') - return -1 - template_file = query.metadata['location'] - - if template_file and (os.path.splitext(template_file)[1] in ['.zip'] or - os.path.exists(os.path.join(template_file, 'template.pros'))): - template = ExternalTemplate(template_file) - query.metadata['location'] = template_file - depot = c.LocalDepot() - logger(__name__).debug(f'Template file found: {template_file}') - else: - if template_file: - logger(__name__).debug(f'Template file exists but is not a valid template: {template_file}') - else: - logger(__name__).error(f'Template not found: {query.name}') - return -1 - template = c.Conductor().resolve_template(query, allow_offline=False) - logger(__name__).debug(f'Template from resolved query: {template}') - if template is None: - logger(__name__).error(f'There are no templates matching {query}!') - return -1 - depot = c.Conductor().get_depot(template.metadata['origin']) - logger(__name__).debug(f'Found depot: {depot}') - # query.metadata contain all of the extra args that also go to the depot. There's no way for us to determine - # whether the arguments are for the template or for the depot, so they share them - logger(__name__).debug(f'Additional depot and template args: {query.metadata}') - c.Conductor().fetch_template(depot, template, **query.metadata) - - -@conductor.command(context_settings={'ignore_unknown_options': True}) -@click.option('--upgrade/--no-upgrade', 'upgrade_ok', default=True, help='Allow upgrading templates in a project') - -@click.option('--install/--no-install', 'install_ok', default=True, help='Allow installing templates in a project') -@click.option('--download/--no-download', 'download_ok', default=True, - help='Allow downloading templates or only allow local templates') -@click.option('--upgrade-user-files/--no-upgrade-user-files', 'force_user', default=False, - help='Replace all user files in a template') -@click.option('--force', 'force_system', default=False, is_flag=True, - help="Force all system files to be inserted into the project") -@click.option('--force-apply', 'force_apply', default=False, is_flag=True, - help="Force apply the template, disregarding if the template is already installed.") -@click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, - help='Remove empty directories when removing files') -@click.option('--early-access/--no-early-access', '--early/--no-early', '-ea/-nea', 'early_access', '--beta/--no-beta', default=None, - help='Create a project using the PROS 4 kernel') -@project_option() -@template_query(required=True) -@default_options -def apply(project: c.Project, query: c.BaseTemplate, **kwargs): - """ - Upgrade or install a template to a PROS project - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("apply-template") - return c.Conductor().apply_template(project, identifier=query, **kwargs) - - -@conductor.command(aliases=['i', 'in'], context_settings={'ignore_unknown_options': True}) -@click.option('--upgrade/--no-upgrade', 'upgrade_ok', default=False) -@click.option('--download/--no-download', 'download_ok', default=True) -@click.option('--force-user', 'force_user', default=False, is_flag=True, - help='Replace all user files in a template') -@click.option('--force-system', '-f', 'force_system', default=False, is_flag=True, - help="Force all system files to be inserted into the project") -@click.option('--force-apply', 'force_apply', default=False, is_flag=True, - help="Force apply the template, disregarding if the template is already installed.") -@click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, - help='Remove empty directories when removing files') -@project_option() -@template_query(required=True) -@default_options -@click.pass_context -def install(ctx: click.Context, **kwargs): - """ - Install a library into a PROS project - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("install-template") - return ctx.invoke(apply, install_ok=True, **kwargs) - - -@conductor.command(context_settings={'ignore_unknown_options': True}, aliases=['u']) -@click.option('--install/--no-install', 'install_ok', default=False) -@click.option('--download/--no-download', 'download_ok', default=True) -@click.option('--force-user', 'force_user', default=False, is_flag=True, - help='Replace all user files in a template') -@click.option('--force-system', '-f', 'force_system', default=False, is_flag=True, - help="Force all system files to be inserted into the project") -@click.option('--force-apply', 'force_apply', default=False, is_flag=True, - help="Force apply the template, disregarding if the template is already installed.") -@click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, - help='Remove empty directories when removing files') -@click.option('--early-access/--no-early-access', '--early/--no-early', '-ea/-nea', 'early_access', '--beta/--no-beta', default=None, - help='Create a project using the PROS 4 kernel') -@project_option() -@template_query(required=False) -@default_options -@click.pass_context -def upgrade(ctx: click.Context, project: c.Project, query: c.BaseTemplate, **kwargs): - """ - Upgrade a PROS project or one of its libraries - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("upgrade-project") - if not query.name: - for template in tuple(project.templates.keys()): - click.secho(f'Upgrading {template}', color='yellow') - q = c.BaseTemplate.create_query(name=template, target=project.target, - supported_kernels=project.templates['kernel'].version) - ctx.invoke(apply, upgrade_ok=True, project=project, query=q, **kwargs) - else: - ctx.invoke(apply, project=project, query=query, upgrade_ok=True, **kwargs) - - -@conductor.command('uninstall') -@click.option('--remove-user', is_flag=True, default=False, help='Also remove user files') -@click.option('--remove-empty-dirs/--no-remove-empty-dirs', 'remove_empty_directories', is_flag=True, default=True, - help='Remove empty directories when removing files') -@click.option('--no-make-clean', is_flag=True, default=True, help='Do not run make clean after removing') -@project_option() -@template_query() -@default_options -def uninstall_template(project: c.Project, query: c.BaseTemplate, remove_user: bool, - remove_empty_directories: bool = False, no_make_clean: bool = False): - """ - Uninstall a template from a PROS project - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("uninstall-template") - c.Conductor().remove_template(project, query, remove_user=remove_user, - remove_empty_directories=remove_empty_directories) - if no_make_clean: - with ui.Notification(): - project.compile(["clean"]) - - -@conductor.command('new-project', aliases=['new', 'create-project']) -@click.argument('path', type=click.Path()) -@click.argument('target', default=c.Conductor().default_target, type=click.Choice(['v5', 'cortex'])) -@click.argument('version', default='latest') -@click.option('--force-user', 'force_user', default=False, is_flag=True, - help='Replace all user files in a template') -@click.option('--force-system', '-f', 'force_system', default=False, is_flag=True, - help="Force all system files to be inserted into the project") -@click.option('--force-refresh', is_flag=True, default=False, show_default=True, - help='Force update all remote depots, ignoring automatic update checks') -@click.option('--no-default-libs', 'no_default_libs', default=False, is_flag=True, - help='Do not install any default libraries after creating the project.') -@click.option('--compile-after', is_flag=True, default=True, show_default=True, - help='Compile the project after creation') -@click.option('--build-cache', is_flag=True, default=None, show_default=False, - help='Build compile commands cache after creation. Overrides --compile-after if both are specified.') -@click.option('--early-access/--no-early-access', '--early/--no-early', '-ea/-nea', 'early_access', '--beta/--no-beta', default=None, - help='Create a project using the PROS 4 kernel') -@click.pass_context -@default_options -def new_project(ctx: click.Context, path: str, target: str, version: str, - force_user: bool = False, force_system: bool = False, - no_default_libs: bool = False, compile_after: bool = True, build_cache: bool = None, **kwargs): - """ - Create a new PROS project - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("new-project") - version_source = version.lower() == 'latest' - if version.lower() == 'latest' or not version: - version = '>0' - if not force_system and c.Project.find_project(path) is not None: - logger(__name__).error('A project already exists in this location at ' + c.Project.find_project(path) + - '! Delete it first. Are you creating a project in an existing one?', extra={'sentry': False}) - ctx.exit(-1) - try: - _conductor = c.Conductor() - if target is None: - target = _conductor.default_target - project = _conductor.new_project(path, target=target, version=version, version_source=version_source, - force_user=force_user, force_system=force_system, - no_default_libs=no_default_libs, **kwargs) - ui.echo('New PROS Project was created:', output_machine=False) - ctx.invoke(info_project, project=project) - - if compile_after or build_cache: - with ui.Notification(): - ui.echo('Building project...') - exit_code = project.compile([], scan_build=build_cache) - if exit_code != 0: - logger(__name__).error(f'Failed to make project: Exit Code {exit_code}', extra={'sentry': False}) - raise click.ClickException('Failed to build') - - except Exception as e: - pros.common.logger(__name__).exception(e) - ctx.exit(-1) - - -@conductor.command('query-templates', - aliases=['search-templates', 'ls-templates', 'lstemplates', 'querytemplates', 'searchtemplates', 'q'], - context_settings={'ignore_unknown_options': True}) -@click.option('--allow-offline/--no-offline', 'allow_offline', default=True, show_default=True, - help='(Dis)allow offline templates in the listing') -@click.option('--allow-online/--no-online', 'allow_online', default=True, show_default=True, - help='(Dis)allow online templates in the listing') -@click.option('--force-refresh', is_flag=True, default=False, show_default=True, - help='Force update all remote depots, ignoring automatic update checks') -@click.option('--limit', type=int, default=15, - help='The maximum number of displayed results for each library') -@click.option('--early-access/--no-early-access', '--early/--no-early', '-ea/-nea', 'early_access', '--beta/--no-beta', default=None, - help='View a list of early access templates') -@template_query(required=False) -@project_option(required=False) -@click.pass_context -@default_options -def query_templates(ctx, project: Optional[c.Project], query: c.BaseTemplate, allow_offline: bool, allow_online: bool, force_refresh: bool, - limit: int, early_access: bool): - """ - Query local and remote templates based on a spec - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("query-templates") - if limit < 0: - limit = 15 - if early_access is None and project is not None: - early_access = project.use_early_access - templates = c.Conductor().resolve_templates(query, allow_offline=allow_offline, allow_online=allow_online, - force_refresh=force_refresh, early_access=early_access) - render_templates = {} - for template in templates: - key = (template.identifier, template.origin) - if key in render_templates: - if isinstance(template, c.LocalTemplate): - render_templates[key]['local'] = True - else: - render_templates[key] = { - 'name': template.name, - 'version': template.version, - 'location': template.origin, - 'target': template.target, - 'local': isinstance(template, c.LocalTemplate) - } - import semantic_version as semver - render_templates = sorted(render_templates.values(), key=lambda k: (k['name'], semver.Version(k['version']), k['local']), reverse=True) - - # Impose the output limit for each library's templates - output_templates = [] - for _, g in groupby(render_templates, key=lambda t: t['name'] + t['target']): - output_templates += list(g)[:limit] - ui.finalize('template-query', output_templates) - - -@conductor.command('info-project') -@click.option('--ls-upgrades/--no-ls-upgrades', 'ls_upgrades', default=False) -@project_option() -@default_options -def info_project(project: c.Project, ls_upgrades): - """ - Display information about a PROS project - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - analytics.send("info-project") - from pros.conductor.project import ProjectReport - report = ProjectReport(project) - _conductor = c.Conductor() - if ls_upgrades: - for template in report.project['templates']: - import semantic_version as semver - templates = _conductor.resolve_templates(c.BaseTemplate.create_query(name=template["name"], - version=f'>{template["version"]}', - target=project.target)) - template["upgrades"] = sorted({t.version for t in templates}, key=lambda v: semver.Version(v), reverse=True) - - ui.finalize('project-report', report) - -@conductor.command('add-depot') -@click.argument('name') -@click.argument('url') -@default_options -def add_depot(name: str, url: str): - """ - Add a depot - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - _conductor = c.Conductor() - _conductor.add_depot(name, url) - - ui.echo(f"Added depot {name} from {url}") - -@conductor.command('remove-depot') -@click.argument('name') -@default_options -def remove_depot(name: str): - """ - Remove a depot - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - _conductor = c.Conductor() - _conductor.remove_depot(name) - - ui.echo(f"Removed depot {name}") - -@conductor.command('query-depots') -@click.option('--url', is_flag=True) -@default_options -def query_depots(url: bool): - """ - Gets all the stored depots - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - _conductor = c.Conductor() - ui.echo(f"Available Depots{' (Add --url for the url)' if not url else ''}:\n") - ui.echo('\n'.join(_conductor.query_depots(url))+"\n") - -@conductor.command('reset') -@click.option('--force', is_flag=True, default=False, help='Force reset') -@default_options -def reset(force: bool): - """ - Reset conductor.pros - - Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more - """ - - if not force: - if not ui.confirm("This will remove all depots and templates. You will be unable to create a new PROS project if you do not have internet connection. Are you sure you want to continue?"): - ui.echo("Aborting") - return - - # Delete conductor.pros - file = os.path.join(click.get_app_dir('PROS'), 'conductor.pros') - if os.path.exists(file): - os.remove(file) - - ui.echo("Conductor was reset") diff --git a/pros/cli/conductor_utils.py b/pros/cli/conductor_utils.py deleted file mode 100644 index cb22cffc..00000000 --- a/pros/cli/conductor_utils.py +++ /dev/null @@ -1,174 +0,0 @@ -import glob -import os.path -import re -import tempfile -import zipfile -from typing import * - -import click -import pros.common.ui as ui -import pros.conductor as c -from pros.common.utils import logger -from pros.conductor.templates import ExternalTemplate -from pros.ga.analytics import analytics -from .common import default_options, template_query -from .conductor import conductor - - -@conductor.command('create-template', context_settings={'allow_extra_args': True, 'ignore_unknown_options': True}) -@click.argument('path', type=click.Path(exists=True)) -@click.argument('name') -@click.argument('version') -@click.option('--system', 'system_files', multiple=True, type=click.Path(), - help='Specify "system" files required by the template') -@click.option('--user', 'user_files', multiple=True, type=click.Path(), - help='Specify files that are intended to be modified by users') -@click.option('--kernels', 'supported_kernels', help='Specify supported kernels') -@click.option('--target', type=click.Choice(['v5', 'cortex']), help='Specify the target platform (cortex or v5)') -@click.option('--destination', type=click.Path(), - help='Specify an alternate destination for the created ZIP file or template descriptor') -@click.option('--zip/--no-zip', 'do_zip', default=True, help='Create a ZIP file or create a template descriptor.') -@default_options -@click.pass_context -def create_template(ctx, path: str, destination: str, do_zip: bool, **kwargs): - """ - Create a template to be used in other projects - - Templates primarily consist of the following fields: name, version, and - files to install. - - Templates have two types of files: system files and user files. User files - are files in a template intended to be modified by users - they are not - replaced during upgrades or removed by default when a library is uninstalled. - System files are files that are for the "system." They get replaced every - time the template is upgraded. The default PROS project is a template. The user - files are files like src/opcontrol.c and src/initialize.c, and the system files - are files like firmware/libpros.a and include/api.h. - - You should specify the --system and --user options multiple times to include - more than one file. Both flags also accept glob patterns. When a glob pattern is - provided and inside a PROS project, then all files that match the pattern that - are NOT supplied by another template are included. - - - Example usage: - - pros conduct create-template . libblrs 2.0.1 --system "firmware/*.a" --system "include/*.h" - """ - analytics.send("create-template") - project = c.Project.find_project(path, recurse_times=1) - if project: - project = c.Project(project) - path = project.location - if not kwargs['supported_kernels'] and kwargs['name'] != 'kernel': - kwargs['supported_kernels'] = f'^{project.kernel}' - kwargs['target'] = project.target - if not destination: - if os.path.isdir(path): - destination = path - else: - destination = os.path.dirname(path) - kwargs['system_files'] = list(kwargs['system_files']) - kwargs['user_files'] = list(kwargs['user_files']) - kwargs['metadata'] = {ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, int(len(ctx.args) / 2) * 2, 2)} - - def get_matching_files(globs: List[str]) -> Set[str]: - matching_files: List[str] = [] - _path = os.path.normpath(path) + os.path.sep - for g in [g for g in globs if glob.has_magic(g)]: - files = glob.glob(f'{path}/{g}', recursive=True) - files = filter(lambda f: os.path.isfile(f), files) - files = [os.path.normpath(os.path.normpath(f).split(_path)[-1]) for f in files] - matching_files.extend(files) - - # matches things like src/opcontrol.{c,cpp} so that we can expand to src/opcontrol.c and src/opcontrol.cpp - pattern = re.compile(r'^([\w{}]+.){{((?:\w+,)*\w+)}}$'.format(os.path.sep.replace('\\', '\\\\'))) - for f in [os.path.normpath(f) for f in globs if not glob.has_magic(f)]: - if re.match(pattern, f): - matches = re.split(pattern, f) - logger(__name__).debug(f'Matches on {f}: {matches}') - matching_files.extend([f'{matches[1]}{ext}' for ext in matches[2].split(',')]) - else: - matching_files.append(f) - - matching_files: Set[str] = set(matching_files) - return matching_files - - matching_system_files: Set[str] = get_matching_files(kwargs['system_files']) - matching_user_files: Set[str] = get_matching_files(kwargs['user_files']) - - matching_system_files: Set[str] = matching_system_files - matching_user_files - - # exclude existing project.pros and template.pros from the template, - # and name@*.zip so that we don't redundantly include ZIPs - exclude_files = {'project.pros', 'template.pros', *get_matching_files([f"{kwargs['name']}@*.zip"])} - if project: - exclude_files = exclude_files.union(project.list_template_files()) - matching_system_files = matching_system_files - exclude_files - matching_user_files = matching_user_files - exclude_files - - def filename_remap(file_path: str) -> str: - if os.path.dirname(file_path) == 'bin': - return file_path.replace('bin', 'firmware', 1) - return file_path - - kwargs['system_files'] = list(map(filename_remap, matching_system_files)) - kwargs['user_files'] = list(map(filename_remap, matching_user_files)) - - if do_zip: - if not os.path.isdir(destination) and os.path.splitext(destination)[-1] != '.zip': - logger(__name__).error(f'{destination} must be a zip file or an existing directory.') - return -1 - with tempfile.TemporaryDirectory() as td: - template = ExternalTemplate(file=os.path.join(td, 'template.pros'), **kwargs) - template.save() - if os.path.isdir(destination): - destination = os.path.join(destination, f'{template.identifier}.zip') - with zipfile.ZipFile(destination, mode='w') as z: - z.write(template.save_file, arcname='template.pros') - - for file in matching_user_files: - source_path = os.path.join(path, file) - dest_file = filename_remap(file) - if os.path.exists(source_path): - ui.echo(f'U: {file}' + (f' -> {dest_file}' if file != dest_file else '')) - z.write(f'{path}/{file}', arcname=dest_file) - for file in matching_system_files: - source_path = os.path.join(path, file) - dest_file = filename_remap(file) - if os.path.exists(source_path): - ui.echo(f'S: {file}' + (f' -> {dest_file}' if file != dest_file else '')) - z.write(f'{path}/{file}', arcname=dest_file) - else: - if os.path.isdir(destination): - destination = os.path.join(destination, 'template.pros') - template = ExternalTemplate(file=destination, **kwargs) - template.save() - - -@conductor.command('purge-template', help='Purge template(s) from the local cache', - context_settings={'ignore_unknown_options': True}) -@click.option('-f', '--force', is_flag=True, default=False, help='Do not prompt for removal of multiple templates') -@template_query(required=False) -@default_options -def purge_template(query: c.BaseTemplate, force): - analytics.send("purge-template") - if not query: - force = click.confirm('Are you sure you want to remove all cached templates? This action is non-reversable!', - abort=True) - cond = c.Conductor() - templates = cond.resolve_templates(query, allow_online=False) - beta_templates = cond.resolve_templates(query, allow_online=False, beta=True) - if len(templates) == 0: - click.echo('No matching templates were found matching the spec.') - return 0 - t_list = [t.identifier for t in templates] + [t.identifier for t in beta_templates] - click.echo(f'The following template(s) will be removed {t_list}') - if len(templates) > 1 and not force: - click.confirm(f'Are you sure you want to remove multiple templates?', abort=True) - for template in templates: - if isinstance(template, c.LocalTemplate): - cond.purge_template(template) - for template in beta_templates: - if isinstance(template, c.LocalTemplate): - cond.purge_template(template) diff --git a/pros/cli/interactive.py b/pros/cli/interactive.py deleted file mode 100644 index 634f1b2f..00000000 --- a/pros/cli/interactive.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -from typing import * -import click -import pros.conductor as c -from .common import PROSGroup, default_options, project_option, pros_root -from pros.ga.analytics import analytics - -@pros_root -def interactive_cli(): - pass - - -@interactive_cli.group(cls=PROSGroup, hidden=True) -@default_options -def interactive(): - pass - - -@interactive.command() -@click.option('--directory', default=os.path.join(os.path.expanduser('~'), 'My PROS Project')) -@default_options -def new_project(directory): - from pros.common.ui.interactive.renderers import MachineOutputRenderer - from pros.conductor.interactive.NewProjectModal import NewProjectModal - app = NewProjectModal(directory=directory) - MachineOutputRenderer(app).run() - - -@interactive.command() -@project_option(required=False, default=None, allow_none=True) -@default_options -def update_project(project: Optional[c.Project]): - from pros.common.ui.interactive.renderers import MachineOutputRenderer - from pros.conductor.interactive.UpdateProjectModal import UpdateProjectModal - app = UpdateProjectModal(project) - MachineOutputRenderer(app).run() - - -@interactive.command() -@project_option(required=False, default=None, allow_none=True) -@default_options -def upload(project: Optional[c.Project]): - from pros.common.ui.interactive.renderers import MachineOutputRenderer - from pros.serial.interactive import UploadProjectModal - MachineOutputRenderer(UploadProjectModal(project)).run() diff --git a/pros/cli/main.py b/pros/cli/main.py deleted file mode 100644 index 8e4d6725..00000000 --- a/pros/cli/main.py +++ /dev/null @@ -1,143 +0,0 @@ -import logging - -# Setup analytics first because it is used by other files - -import os.path - -import pros.common.sentry - -import click -import ctypes -import sys - -import pros.common.ui as ui -import pros.common.ui.log -from pros.cli.click_classes import * -from pros.cli.common import default_options, root_commands -from pros.common.utils import get_version, logger -from pros.ga.analytics import analytics - -import jsonpickle -import pros.cli.build -import pros.cli.conductor -import pros.cli.conductor_utils -import pros.cli.terminal -import pros.cli.upload -import pros.cli.v5_utils -import pros.cli.misc_commands -import pros.cli.interactive -import pros.cli.user_script -import pros.conductor as c - -if sys.platform == 'win32': - kernel32 = ctypes.windll.kernel32 - kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) - -root_sources = [ - 'build', - 'conductor', - 'conductor_utils', - 'terminal', - 'upload', - 'v5_utils', - 'misc_commands', # misc_commands must be after upload so that "pros u" is an alias for upload, not upgrade - 'interactive', - 'user_script' -] - -if getattr(sys, 'frozen', False): - exe_file = sys.executable -else: - exe_file = __file__ - -if os.path.exists(os.path.join(os.path.dirname(exe_file), os.pardir, os.pardir, '.git')): - root_sources.append('test') - -if os.path.exists(os.path.join(os.path.dirname(exe_file), os.pardir, os.pardir, '.git')): - import pros.cli.test - -for root_source in root_sources: - __import__(f'pros.cli.{root_source}') - - -def main(): - try: - ctx_obj = {} - click_handler = pros.common.ui.log.PROSLogHandler(ctx_obj=ctx_obj) - ctx_obj['click_handler'] = click_handler - formatter = pros.common.ui.log.PROSLogFormatter('%(levelname)s - %(name)s:%(funcName)s - %(message)s - pros-cli version:{version}' - .format(version = get_version()), ctx_obj) - click_handler.setFormatter(formatter) - logging.basicConfig(level=logging.WARNING, handlers=[click_handler]) - cli.main(prog_name='pros', obj=ctx_obj, windows_expand_args=False) - except KeyboardInterrupt: - click.echo('Aborted!') - except Exception as e: - logger(__name__).exception(e) - - -def version(ctx: click.Context, param, value): - if not value: - return - ctx.ensure_object(dict) - if ctx.obj.get('machine_output', False): - ui.echo(get_version()) - else: - ui.echo('pros, version {}'.format(get_version())) - ctx.exit(0) - - -def use_analytics(ctx: click.Context, param, value): - if value == None: - return - touse = not analytics.useAnalytics - if str(value).lower().startswith("t"): - touse = True - elif str(value).lower().startswith("f"): - touse = False - else: - ui.echo('Invalid argument provided for \'--use-analytics\'. Try \'--use-analytics=False\' or \'--use-analytics=True\'') - ctx.exit(0) - ctx.ensure_object(dict) - analytics.set_use(touse) - ui.echo(f'Analytics usage set to: {analytics.useAnalytics}') - ctx.exit(0) - -def use_early_access(ctx: click.Context, param, value): - if value is None: - return - conductor = c.Conductor() - value = str(value).lower() - if value.startswith("t") or value in ["1", "yes", "y"]: - conductor.use_early_access = True - elif value.startswith("f") or value in ["0", "no", "n"]: - conductor.use_early_access = False - else: - ui.echo('Invalid argument provided for \'--use-early-access\'. Try \'--use-early-access=False\' or \'--use-early-access=True\'') - ctx.exit(0) - conductor.save() - ui.echo(f'Early access set to: {conductor.use_early_access}') - ctx.exit(0) - - -@click.command('pros', - cls=PROSCommandCollection, - sources=root_commands) -@click.pass_context -@default_options -@click.option('--version', help='Displays version and exits.', is_flag=True, expose_value=False, is_eager=True, - callback=version) -@click.option('--use-analytics', help='Set analytics usage (True/False).', type=str, expose_value=False, - is_eager=True, default=None, callback=use_analytics) -@click.option('--use-early-access', type=str, expose_value=False, is_eager=True, default=None, - help='Create projects with PROS 4 kernel by default', callback=use_early_access) -def cli(ctx): - pros.common.sentry.register() - ctx.call_on_close(after_command) - -def after_command(): - analytics.process_requests() - - -if __name__ == '__main__': - main() diff --git a/pros/cli/misc_commands.py b/pros/cli/misc_commands.py deleted file mode 100644 index d00fbfd3..00000000 --- a/pros/cli/misc_commands.py +++ /dev/null @@ -1,164 +0,0 @@ -import os -from pathlib import Path -import subprocess - -from click.shell_completion import CompletionItem, add_completion_class, ZshComplete - -import pros.common.ui as ui -from pros.cli.common import * -from pros.ga.analytics import analytics - -@pros_root -def misc_commands_cli(): - pass - - -@misc_commands_cli.command() -@click.option('--force-check', default=False, is_flag=True, - help='Force check for updates, disregarding auto-check frequency') -@click.option('--no-install', default=False, is_flag=True, - help='Only check if a new version is available, do not attempt to install') -@default_options -def upgrade(force_check, no_install): - """ - Check for updates to the PROS CLI - """ - with ui.Notification(): - ui.echo('The "pros upgrade" command is currently non-functioning. Did you mean to run "pros c upgrade"?', color='yellow') - - return # Dead code below - - analytics.send("upgrade") - from pros.upgrade import UpgradeManager - manager = UpgradeManager() - manifest = manager.get_manifest(force_check) - ui.logger(__name__).debug(repr(manifest)) - if manager.has_stale_manifest: - ui.logger(__name__).error('Failed to get latest upgrade information. ' - 'Try running with --debug for more information') - return -1 - if not manager.needs_upgrade: - ui.finalize('upgradeInfo', 'PROS CLI is up to date') - else: - ui.finalize('upgradeInfo', manifest) - if not no_install: - if not manager.can_perform_upgrade: - ui.logger(__name__).error(f'This manifest cannot perform the upgrade.') - return -3 - ui.finalize('upgradeComplete', manager.perform_upgrade()) - - -# Script files for each shell -_SCRIPT_FILES = { - 'bash': 'pros-complete.bash', - 'zsh': 'pros-complete.zsh', - 'fish': 'pros.fish', - 'pwsh': 'pros-complete.ps1', - 'powershell': 'pros-complete.ps1', -} - - -def _get_shell_script(shell: str) -> str: - """Get the shell script for the specified shell.""" - script_file = Path(__file__).parent.parent / 'autocomplete' / _SCRIPT_FILES[shell] - with script_file.open('r') as f: - return f.read() - - -@add_completion_class -class PowerShellComplete(ZshComplete): # Identical to ZshComplete except comma delimited instead of newline - """Shell completion for PowerShell and Windows PowerShell.""" - - name = "powershell" - source_template = _get_shell_script("powershell") - - def format_completion(self, item: CompletionItem) -> str: - return super().format_completion(item).replace("\n", ",") - - -@misc_commands_cli.command() -@click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish', 'pwsh', 'powershell']), required=True) -@click.argument('config_path', type=click.Path(resolve_path=True), default=None, required=False) -@click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation prompts') -@default_options -def setup_autocomplete(shell, config_path, force): - """ - Set up autocomplete for PROS CLI - - SHELL: The shell to set up autocomplete for - - CONFIG_PATH: The configuration path to add the autocomplete script to. If not specified, the default configuration - file for the shell will be used. - - Example: pros setup-autocomplete bash ~/.bashrc - """ - - # https://click.palletsprojects.com/en/8.1.x/shell-completion/ - - default_config_paths = { # Default config paths for each shell - 'bash': '~/.bashrc', - 'zsh': '~/.zshrc', - 'fish': '~/.config/fish/completions/', - 'pwsh': None, - 'powershell': None, - } - - # Get the powershell profile path if not specified - if shell in ('pwsh', 'powershell') and config_path is None: - try: - profile_command = f'{shell} -NoLogo -NoProfile -Command "Write-Output $PROFILE"' if os.name == 'nt' else f"{shell} -NoLogo -NoProfile -Command 'Write-Output $PROFILE'" - default_config_paths[shell] = subprocess.run(profile_command, shell=True, capture_output=True, check=True, text=True).stdout.strip() - except subprocess.CalledProcessError as exc: - raise click.UsageError("Failed to determine the PowerShell profile path. Please specify a valid config file.") from exc - - # Use default config path if not specified - if config_path is None: - config_path = default_config_paths[shell] - ui.echo(f"Using default config path {config_path}. To specify a different config path, run 'pros setup-autocomplete {shell} [CONFIG_PATH]'.\n") - config_path = Path(config_path).expanduser().resolve() - - if shell in ('bash', 'zsh', 'pwsh', 'powershell'): - if config_path.is_dir(): - raise click.UsageError(f"Config file {config_path} is a directory. Please specify a valid config file.") - if not config_path.exists(): - raise click.UsageError(f"Config file {config_path} does not exist. Please specify a valid config file.") - - # Write the autocomplete script to a shell script file - script_file = Path(click.get_app_dir("PROS")) / "autocomplete" / _SCRIPT_FILES[shell] - script_file.parent.mkdir(exist_ok=True) - with script_file.open('w') as f: - f.write(_get_shell_script(shell)) - - # Source the autocomplete script in the config file - if shell in ('bash', 'zsh'): - source_autocomplete = f'. "{script_file.as_posix()}"\n' - elif shell in ('pwsh', 'powershell'): - source_autocomplete = f'"{script_file}" | Invoke-Expression\n' - if force or ui.confirm(f"Add the autocomplete script to {config_path}?", default=True): - with config_path.open('r+') as f: - # Only append if the source command is not already in the file - if source_autocomplete not in f.readlines(): - f.write("\n# PROS CLI autocomplete\n") - f.write(source_autocomplete) - else: - ui.echo(f"Autocomplete script written to {script_file}.") - ui.echo(f"Add the following line to {config_path} then restart your shell to enable autocomplete:\n") - ui.echo(source_autocomplete) - return - elif shell == 'fish': - # Check if the config path is a directory or file and set the script directory and file accordingly - if config_path.is_file(): - script_dir = config_path.parent - script_file = config_path - else: - script_dir = config_path - script_file = config_path / _SCRIPT_FILES[shell] - - if not script_dir.exists(): - raise click.UsageError(f"Completions directory {script_dir} does not exist. Please specify a valid completions file or directory.") - - # Write the autocomplete script to a shell script file - with script_file.open('w') as f: - f.write(_get_shell_script(shell)) - - ui.echo(f"Succesfully set up autocomplete for {shell} in {config_path}. Restart your shell to apply changes.") diff --git a/pros/cli/terminal.py b/pros/cli/terminal.py deleted file mode 100644 index 2f05f2fe..00000000 --- a/pros/cli/terminal.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import signal -import time - -import click -import sys - -import pros.conductor as c -import pros.serial.devices as devices -from pros.serial.ports import DirectPort -from pros.common.utils import logger -from .common import default_options, resolve_v5_port, resolve_cortex_port, pros_root -from pros.serial.ports.v5_wireless_port import V5WirelessPort -from pros.ga.analytics import analytics - -@pros_root -def terminal_cli(): - pass - - -@terminal_cli.command() -@default_options -@click.argument('port', default='default') -@click.option('--backend', type=click.Choice(['share', 'solo']), default='solo', - help='Backend port of the terminal. See above for details') -@click.option('--raw', is_flag=True, default=False, - help='Don\'t process the data.') -@click.option('--hex', is_flag=True, default=False, help="Display data as hexadecimal values. Unaffected by --raw") -@click.option('--ports', nargs=2, type=int, default=(None, None), - help='Specify 2 ports for the "share" backend. The default option deterministically selects ports ' - 'based on the serial port name') -@click.option('--banner/--no-banner', 'request_banner', default=True) -@click.option('--output', nargs = 1, type=str, is_eager = True, help='Redirect terminal output to a file', default=None) - -def terminal(port: str, backend: str, **kwargs): - """ - Open a terminal to a serial port - - There are two possible backends for the terminal: "share" or "solo". In "share" mode, a server/bridge is created - so that multiple PROS processes (such as another terminal or flash command) may communicate with the device. In the - simpler solo mode, only one PROS process may communicate with the device. The default mode is "share", but "solo" - may be preferred when "share" doesn't perform adequately. - - Note: share backend is not yet implemented. - """ - analytics.send("terminal") - from pros.serial.devices.vex.v5_user_device import V5UserDevice - from pros.serial.terminal import Terminal - is_v5_user_joystick = False - if port == 'default': - project_path = c.Project.find_project(os.getcwd()) - if project_path is None: - v5_port, is_v5_user_joystick = resolve_v5_port(None, 'user', quiet=True) - cortex_port = resolve_cortex_port(None, quiet=True) - if ((v5_port is None) ^ (cortex_port is None)) or (v5_port is not None and v5_port == cortex_port): - port = v5_port or cortex_port - else: - raise click.UsageError('You must be in a PROS project directory to enable default port selecting') - else: - project = c.Project(project_path) - port = project.target - - if port == 'v5': - port = None - port, is_v5_user_joystick = resolve_v5_port(port, 'user') - elif port == 'cortex': - port = None - port = resolve_cortex_port(port) - kwargs['raw'] = True - if not port: - return -1 - - if backend == 'share': - raise NotImplementedError('Share backend is not yet implemented') - # ser = SerialSharePort(port) - elif is_v5_user_joystick: - logger(__name__).debug("it's a v5 joystick") - ser = V5WirelessPort(port) - else: - logger(__name__).debug("not a v5 joystick") - ser = DirectPort(port) - if kwargs.get('raw', False): - device = devices.RawStreamDevice(ser) - else: - device = devices.vex.V5UserDevice(ser) - term = Terminal(device, request_banner=kwargs.pop('request_banner', True)) - - class TerminalOutput(object): - def __init__(self, file): - self.terminal = sys.stdout - self.log = open(file, 'a') - def write(self, data): - self.terminal.write(data) - self.log.write(data) - def flush(self): - pass - def end(self): - self.log.close() - - output = None - if kwargs.get('output', None): - output_file = kwargs['output'] - output = TerminalOutput(f'{output_file}') - term.console.output = output - sys.stdout = output - logger(__name__).info(f'Redirecting Terminal Output to File: {output_file}') - else: - sys.stdout = sys.__stdout__ - - signal.signal(signal.SIGINT, term.stop) - term.start() - sys.stdout.write("Established terminal connection\n") - sys.stdout.flush() - while not term.alive.is_set(): - time.sleep(0.005) - sys.stdout = sys.__stdout__ - if output: - output.end() - term.join() - logger(__name__).info('CLI Main Thread Dying') diff --git a/pros/cli/test.py b/pros/cli/test.py deleted file mode 100644 index f19ac9a8..00000000 --- a/pros/cli/test.py +++ /dev/null @@ -1,18 +0,0 @@ -from pros.common.ui.interactive.renderers import MachineOutputRenderer -from pros.conductor.interactive.NewProjectModal import NewProjectModal - -from .common import default_options, pros_root - - -@pros_root -def test_cli(): - pass - - -@test_cli.command() -@default_options -def test(): - app = NewProjectModal() - MachineOutputRenderer(app).run() - - # ui.confirm('Hey') diff --git a/pros/cli/upload.py b/pros/cli/upload.py deleted file mode 100644 index 8c234ed8..00000000 --- a/pros/cli/upload.py +++ /dev/null @@ -1,208 +0,0 @@ -from sys import exit -from unicodedata import name - -import pros.common.ui as ui -import pros.conductor as c - -from .common import * -from pros.ga.analytics import analytics - -@pros_root -def upload_cli(): - pass - - -@upload_cli.command(aliases=['u']) -@click.option('--target', type=click.Choice(['v5', 'cortex']), default=None, required=False, - help='Specify the target microcontroller. Overridden when a PROS project is specified.') -@click.argument('path', type=click.Path(exists=True), default=None, required=False) -@click.argument('port', type=str, default=None, required=False) -@project_option(required=False, allow_none=True) -@click.option('--run-after/--no-run-after', 'run_after', default=None, help='Immediately run the uploaded program.', - cls=PROSDeprecated, replacement='after') -@click.option('--run-screen/--execute', 'run_screen', default=None, help='Display run program screen on the brain after upload.', - cls=PROSDeprecated, replacement='after') -@click.option('-af', '--after', type=click.Choice(['run','screen','none']), default=None, help='Action to perform on the brain after upload.', - cls=PROSOption, group='V5 Options') -@click.option('--quirk', type=int, default=0) -@click.option('--slot', default=None, type=click.IntRange(min=1, max=8), help='Program slot on the GUI.', - cls=PROSOption, group='V5 Options') -@click.option('--icon', type=click.Choice(['pros','pizza','planet','alien','ufo','robot','clawbot','question','X','power']), default='pros', - help="Change Program's icon on the V5 Brain", cls=PROSOption, group='V5 Options') -@click.option('--program-version', default=None, type=str, help='Specify version metadata for program.', - cls=PROSOption, group='V5 Options', hidden=True) -@click.option('--ini-config', type=click.Path(exists=True), default=None, help='Specify a program configuration file.', - cls=PROSOption, group='V5 Options', hidden=True) -@click.option('--compress-bin/--no-compress-bin', 'compress_bin', cls=PROSOption, group='V5 Options', default=True, - help='Compress the program binary before uploading.') -@click.option('--description', default="Made with PROS", type=str, cls=PROSOption, group='V5 Options', - help='Change the description displayed for the program.') -@click.option('--name', 'remote_name', default=None, type=str, cls=PROSOption, group='V5 Options', - help='Change the name of the program.') - -@default_options -def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwargs): - """ - Upload a binary to a microcontroller. - - [PATH] may be a directory or file. If a directory, finds a PROS project root and uploads the binary for the correct - target automatically. If a file, then the file is uploaded. Note that --target must be specified in this case. - - [PORT] may be any valid communication port file, such as COM1 or /dev/ttyACM0. If left blank, then a port is - automatically detected based on the target (or as supplied by the PROS project) - """ - analytics.send("upload") - import pros.serial.devices.vex as vex - from pros.serial.ports import DirectPort - kwargs['ide_version'] = project.kernel if not project==None else "None" - kwargs['ide'] = 'PROS' - if path is None or os.path.isdir(path): - if project is None: - project_path = c.Project.find_project(path or os.getcwd()) - if project_path is None: - raise click.UsageError('Specify a file to upload or set the cwd inside a PROS project') - project = c.Project(project_path) - path = os.path.join(project.location, project.output) - if project.target == 'v5' and not kwargs['remote_name']: - kwargs['remote_name'] = project.name - - # apply upload_options as a template - options = dict(**project.upload_options) - if 'port' in options and port is None: - port = options.get('port', None) - if 'slot' in options and kwargs.get('slot', None) is None: - kwargs.pop('slot') - elif kwargs.get('slot', None) is None: - kwargs['slot'] = 1 - if 'icon' in options and kwargs.get('icon','pros') == 'pros': - kwargs.pop('icon') - if 'after' in options and kwargs.get('after','screen') is None: - kwargs.pop('after') - - options.update(kwargs) - kwargs = options - kwargs['target'] = project.target # enforce target because uploading to the wrong uC is VERY bad - if 'program-version' in kwargs: - kwargs['version'] = kwargs['program-version'] - if 'remote_name' not in kwargs: - kwargs['remote_name'] = project.name - name_to_file = { - 'pros' : 'USER902x.bmp', - 'pizza' : 'USER003x.bmp', - 'planet' : 'USER013x.bmp', - 'alien' : 'USER027x.bmp', - 'ufo' : 'USER029x.bmp', - 'clawbot' : 'USER010x.bmp', - 'robot' : 'USER011x.bmp', - 'question' : 'USER002x.bmp', - 'power' : 'USER012x.bmp', - 'X' : 'USER001x.bmp' - } - kwargs['icon'] = name_to_file[kwargs['icon']] - if 'target' not in kwargs or kwargs['target'] is None: - logger(__name__).debug(f'Target not specified. Arguments provided: {kwargs}') - raise click.UsageError('Target not specified. specify a project (using the file argument) or target manually') - if kwargs['target'] == 'v5': - port = resolve_v5_port(port, 'system')[0] - elif kwargs['target'] == 'cortex': - port = resolve_cortex_port(port) - else: - logger(__name__).debug(f"Invalid target provided: {kwargs['target']}") - logger(__name__).debug('Target should be one of ("v5" or "cortex").') - if not port: - raise dont_send(click.UsageError('No port provided or located. Make sure to specify --target if needed.')) - if kwargs['target'] == 'v5': - kwargs['remote_name'] = kwargs['name'] if kwargs.get('name',None) else kwargs['remote_name'] - if kwargs['remote_name'] is None: - kwargs['remote_name'] = os.path.splitext(os.path.basename(path))[0] - kwargs['remote_name'] = kwargs['remote_name'].replace('@', '_') - kwargs['slot'] -= 1 - - action_to_kwarg = { - 'run' : vex.V5Device.FTCompleteOptions.RUN_IMMEDIATELY, - 'screen' : vex.V5Device.FTCompleteOptions.RUN_SCREEN, - 'none' : vex.V5Device.FTCompleteOptions.DONT_RUN - } - after_upload_default = 'screen' - #Determine which FTCompleteOption to assign to run_after - if kwargs['after']==None: - kwargs['after']=after_upload_default - if kwargs['run_after']: - kwargs['after']='run' - elif kwargs['run_screen']==False and not kwargs['run_after']: - kwargs['after']='none' - kwargs['run_after'] = action_to_kwarg[kwargs['after']] - kwargs.pop('run_screen') - kwargs.pop('after') - elif kwargs['target'] == 'cortex': - pass - - logger(__name__).debug('Arguments: {}'.format(str(kwargs))) - # Do the actual uploading! - try: - ser = DirectPort(port) - device = None - if kwargs['target'] == 'v5': - device = vex.V5Device(ser) - elif kwargs['target'] == 'cortex': - device = vex.CortexDevice(ser).get_connected_device() - if project is not None: - device.upload_project(project, **kwargs) - else: - with click.open_file(path, mode='rb') as pf: - device.write_program(pf, **kwargs) - except Exception as e: - logger(__name__).exception(e, exc_info=True) - exit(1) - -@upload_cli.command('lsusb', aliases=['ls-usb', 'ls-devices', 'lsdev', 'list-usb', 'list-devices']) -@click.option('--target', type=click.Choice(['v5', 'cortex']), default=None, required=False) -@default_options -def ls_usb(target): - """ - List plugged in VEX Devices - """ - analytics.send("ls-usb") - from pros.serial.devices.vex import find_v5_ports, find_cortex_ports - - class PortReport(object): - def __init__(self, header: str, ports: List[Any], machine_header: Optional[str] = None): - self.header = header - self.ports = [{'device': p.device, 'desc': p.description} for p in ports] - self.machine_header = machine_header or header - - def __getstate__(self): - return { - 'device_type': self.machine_header, - 'devices': self.ports - } - - def __str__(self): - if len(self.ports) == 0: - return f'There are no connected {self.header}' - else: - port_str = "\n".join([f"{p['device']} - {p['desc']}" for p in self.ports]) - return f'{self.header}:\n{port_str}' - - result = [] - if target == 'v5' or target is None: - ports = find_v5_ports('system') - result.append(PortReport('VEX EDR V5 System Ports', ports, 'v5/system')) - - ports = find_v5_ports('User') - result.append(PortReport('VEX EDR V5 User Ports', ports, 'v5/user')) - if target == 'cortex' or target is None: - ports = find_cortex_ports() - result.append(PortReport('VEX EDR Cortex Microcontroller Ports', ports, 'cortex')) - - ui.finalize('lsusb', result) - - -@upload_cli.command('upload-terminal', aliases=['ut'], hidden=True) -@shadow_command(upload) -@click.pass_context -def make_upload_terminal(ctx, **upload_kwargs): - analytics.send("upload-terminal") - from .terminal import terminal - ctx.invoke(upload, **upload_kwargs) - ctx.invoke(terminal, request_banner=False) diff --git a/pros/cli/user_script.py b/pros/cli/user_script.py deleted file mode 100644 index be0f8259..00000000 --- a/pros/cli/user_script.py +++ /dev/null @@ -1,26 +0,0 @@ -import click - -from pros.common import ui -from .common import default_options, pros_root -from pros.ga.analytics import analytics - -@pros_root -def user_script_cli(): - pass - - -@user_script_cli.command(short_help='Run user script files', hidden=True) -@click.argument('script_file') -@default_options -def user_script(script_file): - """ - Run a script file with the PROS CLI package - """ - analytics.send("user-script") - import os.path - import importlib.util - package_name = os.path.splitext(os.path.split(script_file)[0])[0] - package_path = os.path.abspath(script_file) - ui.echo(f'Loading {package_name} from {package_path}') - spec = importlib.util.spec_from_file_location(package_name, package_path) - spec.loader.load_module() diff --git a/pros/cli/v5_utils.py b/pros/cli/v5_utils.py deleted file mode 100644 index a6fe0eec..00000000 --- a/pros/cli/v5_utils.py +++ /dev/null @@ -1,325 +0,0 @@ -from .common import * -from pros.ga.analytics import analytics - -@pros_root -def v5_utils_cli(): - pass - - -@v5_utils_cli.group(cls=PROSGroup, help='Utilities for managing the VEX V5') -@default_options -def v5(): - pass - - -@v5.command() -@click.argument('port', required=False, default=None) -@default_options -def status(port: str): - """ - Print system information for the V5 - """ - analytics.send("status") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - if ismachineoutput(): - print(device.status) - else: - print('Connected to V5 on {}'.format(port)) - print('System version:', device.status['system_version']) - print('CPU0 F/W version:', device.status['cpu0_version']) - print('CPU1 SDK version:', device.status['cpu1_version']) - print('System ID: 0x{:x}'.format(device.status['system_id'])) - - -@v5.command('ls-files') -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@click.option('--options', type=int, default=0, cls=PROSOption, hidden=True) -@click.argument('port', required=False, default=None) -@default_options -def ls_files(port: str, vid: int, options: int): - """ - List files on the flash filesystem - """ - analytics.send("ls-files") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - c = device.get_dir_count(vid=vid, options=options) - for i in range(0, c): - print(device.get_file_metadata_by_idx(i)) - - -@v5.command(hidden=True) -@click.argument('file_name') -@click.argument('port', required=False, default=None) -@click.argument('outfile', required=False, default=click.get_binary_stream('stdout'), type=click.File('wb')) -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@click.option('--source', type=click.Choice(['ddr', 'flash']), default='flash', cls=PROSOption, hidden=True) -@default_options -def read_file(file_name: str, port: str, vid: int, source: str): - """ - Read file on the flash filesystem to stdout - """ - analytics.send("read-file") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - device.read_file(file=click.get_binary_stream('stdout'), remote_file=file_name, - vid=vid, target=source) - - -@v5.command(hidden=True) -@click.argument('file', type=click.File('rb')) -@click.argument('port', required=False, default=None) -@click.option('--addr', type=int, default=0x03800000, required=False) -@click.option('--remote-file', required=False, default=None) -@click.option('--run-after/--no-run-after', 'run_after', default=False) -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@click.option('--target', type=click.Choice(['ddr', 'flash']), default='flash') -@default_options -def write_file(file, port: str, remote_file: str, **kwargs): - """ - Write a file to the V5. - """ - analytics.send("write-file") - from pros.serial.ports import DirectPort - from pros.serial.devices.vex import V5Device - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - device.write_file(file=file, remote_file=remote_file or os.path.basename(file.name), **kwargs) - - -@v5.command('rm-file') -@click.argument('file_name') -@click.argument('port', required=False, default=None) -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@click.option('--erase-all/--erase-only', 'erase_all', default=False, show_default=True, - help='Erase all files matching base name.') -@default_options -def rm_file(file_name: str, port: str, vid: int, erase_all: bool): - """ - Remove a file from the flash filesystem - """ - analytics.send("rm-file") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - device.erase_file(file_name, vid=vid, erase_all=erase_all) - - -@v5.command('cat-metadata') -@click.argument('file_name') -@click.argument('port', required=False, default=None) -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@default_options -def cat_metadata(file_name: str, port: str, vid: int): - """ - Print metadata for a file - """ - analytics.send("cat-metadata") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - print(device.get_file_metadata_by_name(file_name, vid=vid)) - -@v5.command('rm-program') -@click.argument('slot') -@click.argument('port', type=int, required=False, default=None) -@click.option('--vid', type=int, default=1, cls=PROSOption, hidden=True) -@default_options -def rm_program(slot: int, port: str, vid: int): - """ - Remove a program from the flash filesystem - """ - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return - 1 - - base_name = f'slot_{slot}' - ser = DirectPort(port) - device = V5Device(ser) - device.erase_file(f'{base_name}.ini', vid=vid) - device.erase_file(f'{base_name}.bin', vid=vid) - -@v5.command('rm-all') -@click.argument('port', required=False, default=None) -@click.option('--vid', type=int, default=1, hidden=True, cls=PROSOption) -@default_options -def rm_all(port: str, vid: int): - """ - Remove all user programs from the V5 - """ - analytics.send("rm-all") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - - ser = DirectPort(port) - device = V5Device(ser) - c = device.get_dir_count(vid=vid) - files = [] - for i in range(0, c): - files.append(device.get_file_metadata_by_idx(i)['filename']) - for file in files: - device.erase_file(file, vid=vid) - - -@v5.command(short_help='Run a V5 Program') -@click.argument('slot', required=False, default=1, type=click.IntRange(1, 8)) -@click.argument('port', required=False, default=None) -@default_options -def run(slot: str, port: str): - """ - Run a V5 program - """ - analytics.send("run") - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - file = f'slot_{slot}.bin' - import re - if not re.match(r'[\w\.]{1,24}', file): - logger(__name__).error('file must be a valid V5 filename') - return 1 - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - ser = DirectPort(port) - device = V5Device(ser) - device.execute_program_file(file, run=True) - - -@v5.command(short_help='Stop a V5 Program') -@click.argument('port', required=False, default=None) -@default_options -def stop(port: str): - """ - Stops a V5 program - - If FILE is unspecified or is a directory, then attempts to find the correct filename based on the PROS project - """ - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - ser = DirectPort(port) - device = V5Device(ser) - device.execute_program_file('', run=False) - - -@v5.command(short_help='Take a screen capture of the display') -@click.argument('file_name', required=False, default=None) -@click.argument('port', required=False, default=None) -@click.option('--force', is_flag=True, type=bool, default=False) -@default_options -def capture(file_name: str, port: str, force: bool = False): - """ - Take a screen capture of the display - """ - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - import png - import os - - port = resolve_v5_port(port, 'system')[0] - if not port: - return -1 - ser = DirectPort(port) - device = V5Device(ser) - i_data, width, height = device.capture_screen() - - if i_data is None: - print('Failed to capture screen from connected brain.') - return -1 - - # Sanity checking and default values for filenames - if file_name is None: - import time - time_s = time.strftime('%Y-%m-%d-%H%M%S') - file_name = f'{time_s}_{width}x{height}_pros_capture.png' - if file_name == '-': - # Send the data to stdout to allow for piping - print(i_data, end='') - return - - if not file_name.endswith('.png'): - file_name += '.png' - - if not force and os.path.exists(file_name): - print(f'{file_name} already exists. Refusing to overwrite!') - print('Re-run this command with the --force argument to overwrite existing files.') - return -1 - - with open(file_name, 'wb') as file_: - w = png.Writer(width, height, greyscale=False) - w.write(file_, i_data) - - print(f'Saved screen capture to {file_name}') - -@v5.command('set-variable', aliases=['sv', 'set', 'set_variable'], short_help='Set a kernel variable on a connected V5 device') -@click.argument('variable', type=click.Choice(['teamnumber', 'robotname']), required=True) -@click.argument('value', required=True, type=click.STRING, nargs=1) -@click.argument('port', type=str, default=None, required=False) -@default_options -def set_variable(variable, value, port): - import pros.serial.devices.vex as vex - from pros.serial.ports import DirectPort - - # Get the connected v5 device - port = resolve_v5_port(port, 'system')[0] - if port == None: - return - device = vex.V5Device(DirectPort(port)) - actual_value = device.kv_write(variable, value).decode() - print(f'Value of \'{variable}\' set to : {actual_value}') - -@v5.command('read-variable', aliases=['rv', 'get', 'read_variable'], short_help='Read a kernel variable from a connected V5 device') -@click.argument('variable', type=click.Choice(['teamnumber', 'robotname']), required=True) -@click.argument('port', type=str, default=None, required=False) -@default_options -def read_variable(variable, port): - import pros.serial.devices.vex as vex - from pros.serial.ports import DirectPort - - # Get the connected v5 device - port = resolve_v5_port(port, 'system')[0] - if port == None: - return - device = vex.V5Device(DirectPort(port)) - value = device.kv_read(variable).decode() - print(f'Value of \'{variable}\' is : {value}') diff --git a/pros/common/__init__.py b/pros/common/__init__.py deleted file mode 100644 index 877ae5ff..00000000 --- a/pros/common/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from pros.common.ui import confirm, prompt -from pros.common.utils import dont_send, isdebug, logger, retries diff --git a/pros/common/sentry.py b/pros/common/sentry.py deleted file mode 100644 index 6c0c8690..00000000 --- a/pros/common/sentry.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import * - -import click - -import pros.common.ui as ui - -if TYPE_CHECKING: - from sentry_sdk import Client, Hub, Scope # noqa: F401, flake8 issue with "if TYPE_CHECKING" - import jsonpickle.handlers # noqa: F401, flake8 issue, flake8 issue with "if TYPE_CHECKING" - from pros.config.cli_config import CliConfig # noqa: F401, flake8 issue, flake8 issue with "if TYPE_CHECKING" - -cli_config: 'CliConfig' = None -force_prompt_off = False -SUPPRESSED_EXCEPTIONS = [PermissionError, click.Abort] - -def disable_prompt(): - global force_prompt_off - force_prompt_off = True - -def prompt_to_send(event: Dict[str, Any], hint: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """ - Asks the user for permission to send data to Sentry - """ - global cli_config - with ui.Notification(): - if cli_config is None or (cli_config.offer_sentry is not None and not cli_config.offer_sentry): - return - if force_prompt_off: - ui.logger(__name__).debug('Sentry prompt was forced off through click option') - return - if 'extra' in event and not event['extra'].get('sentry', True): - ui.logger(__name__).debug('Not sending candidate event because event was tagged with extra.sentry = False') - return - if 'exc_info' in hint and (not getattr(hint['exc_info'][1], 'sentry', True) or - any(isinstance(hint['exc_info'][1], t) for t in SUPPRESSED_EXCEPTIONS)): - ui.logger(__name__).debug('Not sending candidate event because exception was tagged with sentry = False') - return - - if not event['tags']: - event['tags'] = dict() - - extra_text = '' - if 'message' in event: - extra_text += event['message'] + '\n' - if 'culprit' in event: - extra_text += event['culprit'] + '\n' - if 'logentry' in event and 'message' in event['logentry']: - extra_text += event['logentry']['message'] + '\n' - if 'exc_info' in hint: - import traceback - extra_text += ''.join(traceback.format_exception(*hint['exc_info'], limit=4)) - - event['tags']['confirmed'] = ui.confirm('We detected something went wrong! Do you want to send a report?', - log=extra_text) - if event['tags']['confirmed']: - ui.echo('Sending bug report.') - - ui.echo(f'Want to get updates? Visit https://pros.cs.purdue.edu/report.html?event={event["event_id"]}') - return event - else: - ui.echo('Not sending bug report.') - - -def add_context(obj: object, override_handlers: bool = True, key: str = None) -> None: - """ - Adds extra metadata to the sentry log - :param obj: Any object (non-primitive) - :param override_handlers: Override some serialization handlers to reduce the output sent to Sentry - :param key: Name of the object to be inserted into the context, may be None to use the classname of obj - """ - - import jsonpickle.handlers # noqa: F811, flake8 issue with "if TYPE_CHECKING" - from pros.conductor.templates import BaseTemplate - - class TemplateHandler(jsonpickle.handlers.BaseHandler): - """ - Override how templates get pickled by JSON pickle - we don't want to send all of the data about a template - from an object - """ - from pros.conductor.templates import BaseTemplate - - def flatten(self, obj: BaseTemplate, data): - rv = { - 'name': obj.name, - 'version': obj.version, - 'target': obj.target, - } - if hasattr(obj, 'location'): - rv['location'] = obj.location - if hasattr(obj, 'origin'): - rv['origin'] = obj.origin - return rv - - def restore(self, obj): - raise NotImplementedError - - if override_handlers: - jsonpickle.handlers.register(BaseTemplate, TemplateHandler, base=True) - - from sentry_sdk import configure_scope - with configure_scope() as scope: - scope.set_extra((key or obj.__class__.__qualname__), jsonpickle.pickler.Pickler(unpicklable=False).flatten(obj)) - - if override_handlers: - jsonpickle.handlers.unregister(BaseTemplate) - - -def add_tag(key: str, value: str): - from sentry_sdk import configure_scope - - with configure_scope() as scope: - scope.set_tag(key, value) - - -def register(cfg: Optional['CliConfig'] = None): - global cli_config, client - if cfg is None: - from pros.config.cli_config import cli_config as get_cli_config - cli_config = get_cli_config() - else: - cli_config = cfg - - assert cli_config is not None - - if cli_config.offer_sentry is False: - return - - import sentry_sdk as sentry - from pros.upgrade import get_platformv2 - - client = sentry.Client( - 'https://00bd27dcded6436cad5c8b2941d6a9d6@sentry.io/1226033', - before_send=prompt_to_send, - release=ui.get_version() - ) - sentry.Hub.current.bind_client(client) - - with sentry.configure_scope() as scope: - scope.set_tag('platformv2', get_platformv2().name) - - -__all__ = ['add_context', 'register', 'add_tag'] diff --git a/pros/common/ui/__init__.py b/pros/common/ui/__init__.py deleted file mode 100644 index 24fcc71d..00000000 --- a/pros/common/ui/__init__.py +++ /dev/null @@ -1,191 +0,0 @@ -import threading - -import jsonpickle -from click._termui_impl import ProgressBar as _click_ProgressBar -from sentry_sdk import add_breadcrumb - -from ..utils import * - -_last_notify_value = 0 -_current_notify_value = 0 -_machine_pickler = jsonpickle.JSONBackend() - - -def _machineoutput(obj: Dict[str, Any]): - click.echo(f'Uc&42BWAaQ{jsonpickle.dumps(obj, unpicklable=False, backend=_machine_pickler)}') - - -def _machine_notify(method: str, obj: Dict[str, Any], notify_value: Optional[int]): - if notify_value is None: - global _current_notify_value - notify_value = _current_notify_value - obj['type'] = f'notify/{method}' - obj['notify_value'] = notify_value - _machineoutput(obj) - - -def echo(text: Any, err: bool = False, nl: bool = True, notify_value: int = None, color: Any = None, - output_machine: bool = True, ctx: Optional[click.Context] = None): - add_breadcrumb(message=text, category='echo') - if ismachineoutput(ctx): - if output_machine: - return _machine_notify('echo', {'text': str(text) + ('\n' if nl else '')}, notify_value) - else: - return click.echo(str(text), nl=nl, err=err, color=color) - - -def confirm(text: str, default: bool = False, abort: bool = False, prompt_suffix: bool = ': ', - show_default: bool = True, err: bool = False, title: AnyStr = 'Please confirm:', - log: str = None): - add_breadcrumb(message=text, category='confirm') - if ismachineoutput(): - from pros.common.ui.interactive.ConfirmModal import ConfirmModal - from pros.common.ui.interactive.renderers import MachineOutputRenderer - - app = ConfirmModal(text, abort, title, log) - rv = MachineOutputRenderer(app).run() - else: - rv = click.confirm(text, default=default, abort=abort, prompt_suffix=prompt_suffix, - show_default=show_default, err=err) - add_breadcrumb(message=f'User responded: {rv}') - return rv - - -def prompt(text, default=None, hide_input=False, - confirmation_prompt=False, type=None, - value_proc=None, prompt_suffix=': ', - show_default=True, err=False): - if ismachineoutput(): - # TODO - pass - else: - return click.prompt(text, default=default, hide_input=hide_input, confirmation_prompt=confirmation_prompt, - type=type, value_proc=value_proc, prompt_suffix=prompt_suffix, show_default=show_default, - err=err) - - -def progressbar(iterable: Iterable = None, length: int = None, label: str = None, show_eta: bool = True, - show_percent: bool = True, show_pos: bool = False, item_show_func: Callable = None, - fill_char: str = '#', empty_char: str = '-', bar_template: str = '%(label)s [%(bar)s] %(info)s', - info_sep: str = ' ', width: int = 36): - if ismachineoutput(): - return _MachineOutputProgressBar(**locals()) - else: - return click.progressbar(**locals()) - - -def finalize(method: str, data: Union[str, Dict, object, List[Union[str, Dict, object, Tuple]]], - human_prefix: Optional[str] = None): - """ - To all those who have to debug this... RIP - """ - - if isinstance(data, str): - human_readable = data - elif isinstance(data, dict): - human_readable = data - elif isinstance(data, List): - if len(data) == 0: - human_readable = '' - elif isinstance(data[0], str): - human_readable = '\n'.join(data) - elif isinstance(data[0], dict) or isinstance(data[0], object): - if hasattr(data[0], '__str__'): - human_readable = '\n'.join([str(d) for d in data]) - else: - if not isinstance(data[0], dict): - data = [d.__dict__ for d in data] - import tabulate - human_readable = tabulate.tabulate([d.values() for d in data], headers=data[0].keys()) - elif isinstance(data[0], tuple): - import tabulate - human_readable = tabulate.tabulate(data[1:], headers=data[0]) - else: - human_readable = data - elif hasattr(data, '__str__'): - human_readable = str(data) - else: - human_readable = data.__dict__ - human_readable = (human_prefix or '') + str(human_readable) - if ismachineoutput(): - _machineoutput({ - 'type': 'finalize', - 'method': method, - 'data': data, - 'human': human_readable - }) - else: - echo(human_readable) - - -class _MachineOutputProgressBar(_click_ProgressBar): - def __init__(self, *args, **kwargs): - global _current_notify_value - kwargs['file'] = open(os.devnull, 'w', encoding='UTF-8') - self.notify_value = kwargs.pop('notify_value', _current_notify_value) - super(_MachineOutputProgressBar, self).__init__(*args, **kwargs) - - def __del__(self): - self.file.close() - - def render_progress(self): - super(_MachineOutputProgressBar, self).render_progress() - obj = {'text': self.label, 'pct': self.pct} - if self.show_eta and self.eta_known and not self.finished: - obj['eta'] = self.eta - _machine_notify('progress', obj, self.notify_value) - - -class Notification(object): - def __init__(self, notify_value: Optional[int] = None): - global _last_notify_value - if not notify_value: - notify_value = _last_notify_value + 1 - if notify_value > _last_notify_value: - _last_notify_value = notify_value - self.notify_value = notify_value - self.old_notify_values = [] - - def __enter__(self): - global _current_notify_value - self.old_notify_values.append(_current_notify_value) - _current_notify_value = self.notify_value - - def __exit__(self, exc_type, exc_val, exc_tb): - global _current_notify_value - _current_notify_value = self.old_notify_values.pop() - - -class EchoPipe(threading.Thread): - def __init__(self, err: bool = False, ctx: Optional[click.Context] = None): - """Setup the object with a logger and a loglevel - and start the thread - """ - self.click_ctx = ctx or click.get_current_context(silent=True) - self.is_err = err - threading.Thread.__init__(self) - self.daemon = False - self.fdRead, self.fdWrite = os.pipe() - self.pipeReader = os.fdopen(self.fdRead, encoding='UTF-8') - self.start() - - def fileno(self): - """Return the write file descriptor of the pipe - """ - return self.fdWrite - - def run(self): - """Run the thread, logging everything. - """ - for line in iter(self.pipeReader.readline, ''): - echo(line.strip('\n'), ctx=self.click_ctx, err=self.is_err) - - self.pipeReader.close() - - def close(self): - """Close the write end of the pipe. - """ - os.close(self.fdWrite) - - -__all__ = ['finalize', 'echo', 'confirm', 'prompt', 'progressbar', 'EchoPipe'] diff --git a/pros/common/ui/interactive/ConfirmModal.py b/pros/common/ui/interactive/ConfirmModal.py deleted file mode 100644 index d4c59235..00000000 --- a/pros/common/ui/interactive/ConfirmModal.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import * - -from . import application, components - - -class ConfirmModal(application.Modal[bool]): - """ - ConfirmModal is used by the ui.confirm() method. - - In --machine-output mode, this Modal is run instead of a textual confirmation request (e.g. click.confirm()) - """ - - def __init__(self, text: str, abort: bool = False, title: AnyStr = 'Please confirm:', log: Optional[AnyStr] = None): - super().__init__(title, will_abort=abort, confirm_button='Yes', cancel_button='No', description=text) - self.log = log - - def confirm(self): - self.set_return(True) - self.exit() - - def cancel(self): - self.set_return(False) - super(ConfirmModal, self).cancel() - - def build(self) -> Generator[components.Component, None, None]: - if self.log: - yield components.VerbatimLabel(self.log) diff --git a/pros/common/ui/interactive/__init__.py b/pros/common/ui/interactive/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pros/common/ui/interactive/application.py b/pros/common/ui/interactive/application.py deleted file mode 100644 index 0db8dfaf..00000000 --- a/pros/common/ui/interactive/application.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import * - -from .components import Component -from .observable import Observable - -P = TypeVar('P') - - -class Application(Observable, Generic[P]): - """ - An Application manages the lifecycle of an interactive UI that is rendered to the users. It creates a view for the - model the application is rendering. - """ - - def build(self) -> Generator[Component, None, None]: - """ - Creates a list of components to render - """ - raise NotImplementedError() - - def __del__(self): - self.exit() - - def on_exit(self, *handlers: Callable): - return super(Application, self).on('end', *handlers) - - def exit(self, **kwargs): - """ - Triggers the renderer to stop the render-read loop. - - :arg return: set the return value before triggering exit. This value would be the value returned by - Renderer.run(Application) - """ - if 'return' in kwargs: - self.set_return(kwargs['return']) - self.trigger('end') - - def on_redraw(self, *handlers: Callable, **kwargs) -> Callable: - return super(Application, self).on('redraw', *handlers, **kwargs) - - def redraw(self) -> None: - self.trigger('redraw') - - def set_return(self, value: P) -> None: - """ - Set the return value of Renderer.run(Application) - """ - self.trigger('return', value) - - def on_return_set(self, *handlers: Callable, **kwargs): - return super(Application, self).on('return', *handlers, **kwargs) - - @classmethod - def get_hierarchy(cls, base: type) -> Optional[List[str]]: - """ - Returns the list of classes this object subclasses. - - Needed by receivers to know how to interpret the Application. The renderer may not know how to render - UploadProjectModal, but does know how to render a Modal. - For UploadProjectModal, ['UploadProjectModal', 'Modal', 'Application'] is returned - """ - if base == cls: - return [base.__name__] - for t in base.__bases__: - hierarchy = cls.get_hierarchy(t) - if hierarchy: - hierarchy.insert(0, base.__name__) - return hierarchy - return None - - def __getstate__(self): - """ - Returns the dictionary representation of this Application - """ - return dict( - etype=Application.get_hierarchy(self.__class__), - elements=[e.__getstate__() for e in self.build()], - uuid=self.uuid - ) - - -class Modal(Application[P], Generic[P]): - """ - An Application which is typically displayed in a pop-up box. It has a title, description, continue button, - and cancel button. - """ - # title of the modal to be displayed - title: AnyStr - # optional description displayed underneath the Modal - description: Optional[AnyStr] - # If true, the cancel button will cause the CLI to exit. Interactive UI parsers should kill the CLI process to - # guarantee this property - will_abort: bool - # Confirmation button text - confirm_button: AnyStr - # Cancel button text - cancel_button: AnyStr - - def __init__(self, title: AnyStr, description: Optional[AnyStr] = None, - will_abort: bool = True, confirm_button: AnyStr = 'Continue', cancel_button: AnyStr = 'Cancel', - can_confirm: Optional[bool] = None): - super().__init__() - self.title = title - self.description = description - self.will_abort = will_abort - self.confirm_button = confirm_button - self.cancel_button = cancel_button - self._can_confirm = can_confirm - - self.on('confirm', self._confirm) - - def on_cancel(): - nonlocal self - self.cancel() - - self.on('cancel', on_cancel) - - def confirm(self, *args, **kwargs): - raise NotImplementedError() - - def cancel(self, *args, **kwargs): - self.exit() - - @property - def can_confirm(self): - if self._can_confirm is not None: - return self._can_confirm - return True - - def build(self) -> Generator[Component, None, None]: - raise NotImplementedError() - - def __getstate__(self): - extra_state = {} - if self.description is not None: - extra_state['description'] = self.description - return dict( - **super(Modal, self).__getstate__(), - **extra_state, - title=self.title, - will_abort=self.will_abort, - confirm_button=self.confirm_button, - cancel_button=self.cancel_button, - can_confirm=self.can_confirm - ) - - def _confirm(self, *args, **kwargs): - """ - Triggered by "confirm" response. We should check if the Modal is actually eligible to confirm. If not, redraw it - since there may be some new information to display to user - """ - if self.can_confirm: - self.confirm(*args, **kwargs) - else: - self.redraw() diff --git a/pros/common/ui/interactive/components/__init__.py b/pros/common/ui/interactive/components/__init__.py deleted file mode 100644 index e470f931..00000000 --- a/pros/common/ui/interactive/components/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .button import Button -from .checkbox import Checkbox -from .component import Component -from .container import Container -from .input import DirectorySelector, FileSelector, InputBox -from .input_groups import ButtonGroup, DropDownBox -from .label import Label, Spinner, VerbatimLabel - -__all__ = ['Component', 'Button', 'Container', 'InputBox', 'ButtonGroup', 'DropDownBox', 'Label', - 'DirectorySelector', 'FileSelector', 'Checkbox', 'Spinner', 'VerbatimLabel'] diff --git a/pros/common/ui/interactive/components/button.py b/pros/common/ui/interactive/components/button.py deleted file mode 100644 index a3716158..00000000 --- a/pros/common/ui/interactive/components/button.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import * - -from .component import Component -from ..observable import Observable - - -class Button(Component, Observable): - """ - An Observable Component that represents a Button with some text - """ - - def __init__(self, text: AnyStr): - super().__init__() - self.text = text - - def on_clicked(self, *handlers: Callable, **kwargs): - return self.on('clicked', *handlers, **kwargs) - - def __getstate__(self) -> dict: - return dict( - **super(Button, self).__getstate__(), - text=self.text, - uuid=self.uuid - ) diff --git a/pros/common/ui/interactive/components/checkbox.py b/pros/common/ui/interactive/components/checkbox.py deleted file mode 100644 index dc6ec0b8..00000000 --- a/pros/common/ui/interactive/components/checkbox.py +++ /dev/null @@ -1,6 +0,0 @@ -from pros.common.ui.interactive.components.component import BasicParameterizedComponent -from pros.common.ui.interactive.parameters import BooleanParameter - - -class Checkbox(BasicParameterizedComponent[BooleanParameter]): - pass diff --git a/pros/common/ui/interactive/components/component.py b/pros/common/ui/interactive/components/component.py deleted file mode 100644 index 158fc0bc..00000000 --- a/pros/common/ui/interactive/components/component.py +++ /dev/null @@ -1,76 +0,0 @@ -from typing import * - -from pros.common.ui.interactive.parameters.parameter import Parameter -from pros.common.ui.interactive.parameters.validatable_parameter import ValidatableParameter - - -class Component(object): - """ - A Component is the basic building block of something to render to users. - - Components must convey type. For backwards compatibility, Components will advertise their class hierarchy to - the renderer so that it may try to render something reasonable if the renderer hasn't implemented a handler - for the specific component class. - For instance, DropDownComponent is a subclass of BasicParameterComponent, ParameterizedComponent, and finally - Component. If a renderer has not implemented DropDownComponent, then it can render its version of a - BasicParameterComponent (or ParameterizedComponent). Although a dropdown isn't rendered to the user, something - reasonable can still be displayed. - """ - - @classmethod - def get_hierarchy(cls, base: type) -> Optional[List[str]]: - if base == cls: - return [base.__name__] - for t in base.__bases__: - lst = cls.get_hierarchy(t) - if lst: - lst.insert(0, base.__name__) - return lst - return None - - def __getstate__(self) -> Dict: - return dict( - etype=Component.get_hierarchy(self.__class__) - ) - - -P = TypeVar('P', bound=Parameter) - - -class ParameterizedComponent(Component, Generic[P]): - """ - A ParameterizedComponent has a parameter which takes a value - """ - - def __init__(self, parameter: P): - self.parameter = parameter - - def __getstate__(self): - extra_state = {} - if isinstance(self.parameter, ValidatableParameter): - extra_state['valid'] = self.parameter.is_valid() - reason = self.parameter.is_valid_reason() - if reason: - extra_state['valid_reason'] = self.parameter.is_valid_reason() - return dict( - **super(ParameterizedComponent, self).__getstate__(), - **extra_state, - value=self.parameter.value, - uuid=self.parameter.uuid, - ) - - -class BasicParameterizedComponent(ParameterizedComponent[P], Generic[P]): - """ - A BasicParameterComponent is a ParameterizedComponent with a label. - """ - - def __init__(self, label: AnyStr, parameter: P): - super().__init__(parameter) - self.label = label - - def __getstate__(self): - return dict( - **super(BasicParameterizedComponent, self).__getstate__(), - text=self.label, - ) diff --git a/pros/common/ui/interactive/components/container.py b/pros/common/ui/interactive/components/container.py deleted file mode 100644 index 8b8615f4..00000000 --- a/pros/common/ui/interactive/components/container.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import * - -from pros.common.ui.interactive.parameters import BooleanParameter -from .component import Component - - -class Container(Component): - """ - A Container has multiple Components, possibly a title, and possibly a description - """ - - def __init__(self, *elements: Component, - title: Optional[AnyStr] = None, description: Optional[AnyStr] = None, - collapsed: Union[BooleanParameter, bool] = False): - self.title = title - self.description = description - self.elements = elements - self.collapsed = BooleanParameter(collapsed) if isinstance(collapsed, bool) else collapsed - - def __getstate__(self): - extra_state = { - 'uuid': self.collapsed.uuid, - 'collapsed': self.collapsed.value - } - if self.title is not None: - extra_state['title'] = self.title - if self.description is not None: - extra_state['description'] = self.description - return dict( - **super(Container, self).__getstate__(), - **extra_state, - elements=[e.__getstate__() for e in self.elements] - ) diff --git a/pros/common/ui/interactive/components/input.py b/pros/common/ui/interactive/components/input.py deleted file mode 100644 index 8d35b5e8..00000000 --- a/pros/common/ui/interactive/components/input.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import * - -from .component import BasicParameterizedComponent, P - - -class InputBox(BasicParameterizedComponent[P], Generic[P]): - """ - An InputBox is a Component with a Parameter that is rendered with an input box - """ - - def __init__(self, label: AnyStr, parameter: P, placeholder: Optional = None): - super(InputBox, self).__init__(label, parameter) - self.placeholder = placeholder - - def __getstate__(self) -> dict: - extra_state = {} - if self.placeholder is not None: - extra_state['placeholder'] = self.placeholder - return dict( - **super(InputBox, self).__getstate__(), - **extra_state, - ) - - -class FileSelector(InputBox[P], Generic[P]): - pass - - -class DirectorySelector(InputBox[P], Generic[P]): - pass diff --git a/pros/common/ui/interactive/components/input_groups.py b/pros/common/ui/interactive/components/input_groups.py deleted file mode 100644 index 93171cfd..00000000 --- a/pros/common/ui/interactive/components/input_groups.py +++ /dev/null @@ -1,14 +0,0 @@ -from pros.common.ui.interactive.parameters.misc_parameters import OptionParameter -from .component import BasicParameterizedComponent - - -class DropDownBox(BasicParameterizedComponent[OptionParameter]): - def __getstate__(self): - return dict( - **super(DropDownBox, self).__getstate__(), - options=self.parameter.options - ) - - -class ButtonGroup(DropDownBox): - pass diff --git a/pros/common/ui/interactive/components/label.py b/pros/common/ui/interactive/components/label.py deleted file mode 100644 index 8b060300..00000000 --- a/pros/common/ui/interactive/components/label.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import * - -from .component import Component - - -class Label(Component): - def __init__(self, text: AnyStr): - self.text = text - - def __getstate__(self): - return dict( - **super(Label, self).__getstate__(), - text=self.text - ) - - -class VerbatimLabel(Label): - """ - Should be displayed with a monospace font - """ - pass - - -class Spinner(Label): - """ - Spinner is a component which indicates to the user that something is happening in the background - """ - - def __init__(self): - super(Spinner, self).__init__('Loading...') diff --git a/pros/common/ui/interactive/observable.py b/pros/common/ui/interactive/observable.py deleted file mode 100644 index ec8b0855..00000000 --- a/pros/common/ui/interactive/observable.py +++ /dev/null @@ -1,78 +0,0 @@ -from functools import wraps -from typing import * -from uuid import uuid4 as uuid - -import observable - -from pros.common import logger - -_uuid_table = dict() # type: Dict[str, Observable] - - -class Observable(observable.Observable): - """ - Wrapper class for the observable package for use in interactive UI. It registers itself with a global registry - to facilitate updates from any context (e.g. from a renderer). - """ - - @classmethod - def notify(cls, uuid, event, *args, **kwargs): - """ - Triggers an Observable given its UUID. See arguments for Observable.trigger - """ - if isinstance(uuid, Observable): - uuid = uuid.uuid - if uuid in _uuid_table: - _uuid_table[uuid].trigger(event, *args, **kwargs) - else: - logger(__name__).warning(f'Could not find an Observable to notify with UUID: {uuid}', sentry=True) - - def on(self, event, *handlers, - bound_args: Tuple[Any, ...] = None, bound_kwargs: Dict[str, Any] = None, - asynchronous: bool = False) -> Callable: - """ - Sets up a callable to be called whenenver "event" is triggered - :param event: Event to bind to. Most classes expose an e.g. "on_changed" wrapper which provides the correct - event string - :param handlers: A list of Callables to call when event is fired - :param bound_args: Bind ordered arguments to the Callable. These are supplied before the event's supplied - arguments - :param bound_kwargs: Bind keyword arguments to the Callable. These are supplied before the event's supplied - kwargs. They should not conflict with the supplied event kwargs - :param asynchronous: If true, the Callable will be called in a new thread. Useful if the work to be done from - an event takes a long time to process - :return: - """ - if bound_args is None: - bound_args = [] - if bound_kwargs is None: - bound_kwargs = {} - - if asynchronous: - def bind(h): - def bound(*args, **kw): - from threading import Thread - from pros.common.utils import with_click_context - t = Thread(target=with_click_context(h), args=(*bound_args, *args), kwargs={**bound_kwargs, **kw}) - t.start() - return t - - return bound - else: - def bind(h): - @wraps(h) - def bound(*args, **kw): - return h(*bound_args, *args, **bound_kwargs, **kw) - - return bound - - return super(Observable, self).on(event, *[bind(h) for h in handlers]) - - def trigger(self, event, *args, **kw): - logger(__name__).debug(f'Triggered {self.uuid} ({type(self).__name__}) "{event}" event: {args} {kw}') - return super().trigger(event, *args, **kw) - - def __init__(self): - self.uuid = str(uuid()) - _uuid_table[self.uuid] = self - super(Observable, self).__init__() diff --git a/pros/common/ui/interactive/parameters/__init__.py b/pros/common/ui/interactive/parameters/__init__.py deleted file mode 100644 index 55c5dafe..00000000 --- a/pros/common/ui/interactive/parameters/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .misc_parameters import BooleanParameter, OptionParameter, RangeParameter -from .parameter import Parameter -from .validatable_parameter import AlwaysInvalidParameter, ValidatableParameter - -__all__ = ['Parameter', 'OptionParameter', 'BooleanParameter', 'ValidatableParameter', 'RangeParameter', - 'AlwaysInvalidParameter'] diff --git a/pros/common/ui/interactive/parameters/misc_parameters.py b/pros/common/ui/interactive/parameters/misc_parameters.py deleted file mode 100644 index f19edba9..00000000 --- a/pros/common/ui/interactive/parameters/misc_parameters.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import * - -from pros.common.ui.interactive.parameters.parameter import Parameter -from pros.common.ui.interactive.parameters.validatable_parameter import ValidatableParameter - -T = TypeVar('T') - - -class OptionParameter(ValidatableParameter, Generic[T]): - def __init__(self, initial_value: T, options: List[T]): - super().__init__(initial_value) - self.options = options - - def validate(self, value: Any): - return value in self.options - - -class BooleanParameter(Parameter[bool]): - def update(self, new_value): - true_prefixes = ['T', 'Y'] - true_matches = ['1'] - v = str(new_value).upper() - is_true = v in true_matches or any(v.startswith(p) for p in true_prefixes) - super(BooleanParameter, self).update(is_true) - - -class RangeParameter(ValidatableParameter[int]): - def __init__(self, initial_value: int, range: Tuple[int, int]): - super().__init__(initial_value) - self.range = range - - def validate(self, value: T): - if self.range[0] <= value <= self.range[1]: - return True - else: - return f'{value} is not within [{self.range[0]}, {self.range[1]}]' - - def update(self, new_value): - super(RangeParameter, self).update(int(new_value)) diff --git a/pros/common/ui/interactive/parameters/parameter.py b/pros/common/ui/interactive/parameters/parameter.py deleted file mode 100644 index 1c11eb5e..00000000 --- a/pros/common/ui/interactive/parameters/parameter.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import * - -from pros.common.ui.interactive.observable import Observable - -T = TypeVar('T') - - -class Parameter(Observable, Generic[T]): - """ - A Parameter is an observable value. - - Triggering the "update" event will cause the value to update. - The Parameter will trigger a "changed" event if the value was updated - """ - - def __init__(self, initial_value: T): - super().__init__() - self.value = initial_value - - self.on('update', self.update) - - def update(self, new_value): - self.value = new_value - self.trigger('changed', self) - - def on_changed(self, *handlers: Callable, **kwargs): - return self.on('changed', *handlers, **kwargs) diff --git a/pros/common/ui/interactive/parameters/validatable_parameter.py b/pros/common/ui/interactive/parameters/validatable_parameter.py deleted file mode 100644 index ceafd59f..00000000 --- a/pros/common/ui/interactive/parameters/validatable_parameter.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import * - -from pros.common.ui.interactive.parameters.parameter import Parameter - -T = TypeVar('T') - - -class ValidatableParameter(Parameter, Generic[T]): - """ - A ValidatableParameter is a parameter which has some restriction on valid values. - - By default, on_changed will subscribe to valid value changes, e.g. only when the Parameter's value is valid does - the callback get invoked. This event tag is "changed_validated" - """ - - def __init__(self, initial_value: T, allow_invalid_input: bool = True, - validate: Optional[Callable[[T], Union[bool, str]]] = None): - """ - :param allow_invalid_input: Allow invalid input to be propagated to the `changed` event - """ - super().__init__(initial_value) - self.allow_invalid_input = allow_invalid_input - self.validate_lambda = validate or (lambda v: bool(v)) - - def validate(self, value: T) -> Union[bool, str]: - return self.validate_lambda(value) - - def is_valid(self, value: T = None) -> bool: - rv = self.validate(value if value is not None else self.value) - if isinstance(rv, bool): - return rv - else: - return False - - def is_valid_reason(self, value: T = None) -> Optional[str]: - rv = self.validate(value if value is not None else self.value) - return rv if isinstance(rv, str) else None - - def update(self, new_value): - if self.allow_invalid_input or self.is_valid(new_value): - super(ValidatableParameter, self).update(new_value) - if self.is_valid(): - self.trigger('changed_validated', self) - - def on_changed(self, *handlers: Callable, **kwargs): - """ - Subscribe to event whenever value validly changes - """ - return self.on('changed_validated', *handlers, **kwargs) - - def on_any_changed(self, *handlers: Callable, **kwargs): - """ - Subscribe to event whenever value changes (regardless of whether or not new value is valid) - """ - return self.on('changed', *handlers, **kwargs) - - -class AlwaysInvalidParameter(ValidatableParameter[T], Generic[T]): - def validate(self, value: T): - return False diff --git a/pros/common/ui/interactive/renderers/MachineOutputRenderer.py b/pros/common/ui/interactive/renderers/MachineOutputRenderer.py deleted file mode 100644 index 4bb5eddb..00000000 --- a/pros/common/ui/interactive/renderers/MachineOutputRenderer.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -from threading import Semaphore, current_thread -from typing import * - -import click - -from pros.common import ui -from pros.common.ui.interactive.observable import Observable -from .Renderer import Renderer -from ..application import Application - -current: List['MachineOutputRenderer'] = [] - - -def _push_renderer(renderer: 'MachineOutputRenderer'): - global current - - stack: List['MachineOutputRenderer'] = current - stack.append(renderer) - - -def _remove_renderer(renderer: 'MachineOutputRenderer'): - global current - - stack: List['MachineOutputRenderer'] = current - if renderer in stack: - stack.remove(renderer) - - -def _current_renderer() -> Optional['MachineOutputRenderer']: - global current - - stack: List['MachineOutputRenderer'] = current - return stack[-1] if len(stack) > 0 else None - - -P = TypeVar('P') - - -class MachineOutputRenderer(Renderer[P], Generic[P]): - def __init__(self, app: Application[P]): - global current - - super().__init__(app) - self.alive = False - self.thread = None - self.stop_sem = Semaphore(0) - - @app.on_redraw - def on_redraw(): - self.render(self.app) - - app.on_exit(lambda: self.stop()) - - @staticmethod - def get_line(): - line = click.get_text_stream('stdin').readline().strip() - return line.strip() if line is not None else None - - def run(self) -> P: - _push_renderer(self) - self.thread = current_thread() - self.alive = True - while self.alive: - self.render(self.app) - if not self.alive: - break - - line = self.get_line() - if not self.alive or not line or line.isspace(): - continue - - try: - value = json.loads(line) - if 'uuid' in value and 'event' in value: - Observable.notify(value['uuid'], value['event'], *value.get('args', []), **value.get('kwargs', {})) - except json.JSONDecodeError as e: - ui.logger(__name__).exception(e) - except BaseException as e: - ui.logger(__name__).exception(e) - break - self.stop_sem.release() - self.stop() - return self.run_rv - - def stop(self): - ui.logger(__name__).debug(f'Stopping {self.app}') - self.alive = False - - if current_thread() != self.thread: - ui.logger(__name__).debug(f'Interrupting render thread of {self.app}') - while not self.stop_sem.acquire(timeout=0.1): - self.wake_me() - - ui.logger(__name__).debug(f'Broadcasting stop {self.app}') - self._output({ - 'uuid': self.app.uuid, - 'should_exit': True - }) - - _remove_renderer(self) - top_renderer = _current_renderer() - if top_renderer: - top_renderer.wake_me() - - def wake_me(self): - """ - Hack to wake up input thread to know to shut down - """ - ui.logger(__name__).debug(f'Broadcasting WAKEME for {self.app}') - if ui.ismachineoutput(): - ui._machineoutput({'type': 'wakeme'}) - else: - ui.echo('Wake up the renderer!') - - @staticmethod - def _output(data: dict): - data['type'] = 'input/interactive' - if ui.ismachineoutput(): - ui._machineoutput(data) - else: - ui.echo(str(data)) - - def render(self, app: Application) -> None: - if self.alive: - self._output(app.__getstate__()) diff --git a/pros/common/ui/interactive/renderers/Renderer.py b/pros/common/ui/interactive/renderers/Renderer.py deleted file mode 100644 index 40f17a0e..00000000 --- a/pros/common/ui/interactive/renderers/Renderer.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import * - -from ..application import Application - -P = TypeVar('P') - - -class Renderer(Generic[P]): - """ - The Renderer is responsible for: - - Rendering the application in a manner that is accepted by the presenter - - Triggering events that the presenter tells us about - - Returning a value to the callee - """ - - def __init__(self, app: Application[P]): - self.app = app - self.run_rv: Any = None - - @app.on_return_set - def on_return_set(value): - self.run_rv = value - - def render(self, app: Application[P]) -> None: - raise NotImplementedError() - - def run(self) -> P: - raise NotImplementedError() diff --git a/pros/common/ui/interactive/renderers/__init__.py b/pros/common/ui/interactive/renderers/__init__.py deleted file mode 100644 index b032cb28..00000000 --- a/pros/common/ui/interactive/renderers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .MachineOutputRenderer import MachineOutputRenderer diff --git a/pros/common/ui/log.py b/pros/common/ui/log.py deleted file mode 100644 index 8202ef95..00000000 --- a/pros/common/ui/log.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging - -import click -import jsonpickle - -from pros.common import isdebug - -_machine_pickler = jsonpickle.JSONBackend() - - -class PROSLogHandler(logging.StreamHandler): - """ - A subclass of logging.StreamHandler so that we can correctly encapsulate logging messages - """ - - def __init__(self, *args, ctx_obj=None, **kwargs): - # Need access to the raw ctx_obj in case an exception is thrown before the context has - # been initialized (e.g. when argument parsing is happening) - self.ctx_obj = ctx_obj - super().__init__(*args, **kwargs) - - def emit(self, record): - try: - if self.ctx_obj.get('machine_output', False): - formatter = self.formatter or logging.Formatter() - record.message = record.getMessage() - obj = { - 'type': 'log/message', - 'level': record.levelname, - 'message': formatter.formatMessage(record), - 'simpleMessage': record.message - } - if record.exc_info: - obj['trace'] = formatter.formatException(record.exc_info) - msg = f'Uc&42BWAaQ{jsonpickle.dumps(obj, unpicklable=False, backend=_machine_pickler)}' - else: - msg = self.format(record) - click.echo(msg) - except Exception: - self.handleError(record) - - -class PROSLogFormatter(logging.Formatter): - """ - A subclass of the logging.Formatter so that we can print full exception traces ONLY if we're in debug mode - """ - - def formatException(self, ei): - if not isdebug(): - return '\n'.join(super().formatException(ei).split('\n')[-3:]) - else: - return super().formatException(ei) diff --git a/pros/common/utils.py b/pros/common/utils.py deleted file mode 100644 index 294da89f..00000000 --- a/pros/common/utils.py +++ /dev/null @@ -1,149 +0,0 @@ -import logging -import os -import os.path -import sys -from functools import lru_cache, wraps -from typing import * - -import click - -import pros - - -@lru_cache(1) -def get_version(): - try: - ver = open(os.path.join(os.path.dirname(__file__), '..', '..', 'version')).read().strip() - if ver is not None: - return ver - except: - pass - try: - if getattr(sys, 'frozen', False): - import _constants - ver = _constants.CLI_VERSION - if ver is not None: - return ver - except: - pass - try: - import pkg_resources - except ImportError: - pass - else: - import pros.cli.main - module = pros.cli.main.__name__ - for dist in pkg_resources.working_set: - scripts = dist.get_entry_map().get('console_scripts') or {} - for script_name, entry_point in iter(scripts.items()): - if entry_point.module_name == module: - ver = dist.version - if ver is not None: - return ver - raise RuntimeError('Could not determine version') - - -def retries(func, retry: int = 3): - @wraps(func) - def retries_wrapper(*args, n_retries: int = retry, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - if n_retries > 0: - return retries_wrapper(*args, n_retries=n_retries - 1, **kwargs) - else: - raise e - - return retries_wrapper - - -def logger(obj: Union[str, object] = pros.__name__) -> logging.Logger: - if isinstance(obj, str): - return logging.getLogger(obj) - return logging.getLogger(obj.__module__) - - -def isdebug(obj: Union[str, object] = pros.__name__) -> bool: - if obj is None: - obj = pros.__name__ - if isinstance(obj, str): - return logging.getLogger(obj).getEffectiveLevel() == logging.DEBUG - return logging.getLogger(obj.__module__).getEffectiveLevel() == logging.DEBUG - - -def ismachineoutput(ctx: click.Context = None) -> bool: - if ctx is None: - ctx = click.get_current_context(silent=True) - if isinstance(ctx, click.Context): - ctx.ensure_object(dict) - assert isinstance(ctx.obj, dict) - return ctx.obj.get('machine_output', False) - else: - return False - - -def get_pros_dir(): - return click.get_app_dir('PROS') - - -def with_click_context(func): - ctx = click.get_current_context(silent=True) - if not ctx or not isinstance(ctx, click.Context): - return func - else: - def _wrap(*args, **kwargs): - with ctx: - try: - return func(*args, **kwargs) - except BaseException as e: - logger(__name__).exception(e) - - return _wrap - - -def download_file(url: str, ext: Optional[str] = None, desc: Optional[str] = None) -> Optional[str]: - """ - Helper method to download a temporary file. - :param url: URL of the file to download - :param ext: Expected extension of the file to be downloaded - :param desc: Description of file being downloaded (for progressbar) - :return: The path of the downloaded file, or None if there was an error - """ - import requests - from pros.common.ui import progressbar - # from rfc6266_parser import parse_requests_response - import re - - response = requests.get(url, stream=True) - if response.status_code == 200: - filename: str = url.rsplit('/', 1)[-1] - if 'Content-Disposition' in response.headers.keys(): - filename = re.findall("filename=(.+)", response.headers['Content-Disposition'])[0] - # try: - # disposition = parse_requests_response(response) - # if isinstance(ext, str): - # filename = disposition.filename_sanitized(ext) - # else: - # filename = disposition.filename_unsafe - # except RuntimeError: - # pass - output_path = os.path.join(get_pros_dir(), 'download', filename) - - if os.path.exists(output_path): - os.remove(output_path) - elif not os.path.exists(os.path.dirname(output_path)): - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - with open(output_path, mode='wb') as file: - with progressbar(length=int(response.headers['Content-Length']), - label=desc or f'Downloading {filename}') as pb: - for chunk in response.iter_content(256): - file.write(chunk) - pb.update(len(chunk)) - return output_path - return None - - -def dont_send(e: Exception): - e.sentry = False - return e diff --git a/pros/conductor/__init__.py b/pros/conductor/__init__.py deleted file mode 100644 index 9d8c0406..00000000 --- a/pros/conductor/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ['BaseTemplate', 'Template', 'LocalTemplate', 'Depot', 'LocalDepot', 'Project', 'Conductor'] - -from .conductor import Conductor -from .depots import Depot, LocalDepot -from .project import Project -from .templates import BaseTemplate, Template, LocalTemplate diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py deleted file mode 100644 index 53129cf3..00000000 --- a/pros/conductor/conductor.py +++ /dev/null @@ -1,405 +0,0 @@ -import errno -import os.path -import shutil -from enum import Enum -from pathlib import Path -import sys -from typing import * -import re - -import click -from semantic_version import Spec, Version - -from pros.common import * -from pros.conductor.project import TemplateAction -from pros.conductor.project.template_resolution import InvalidTemplateException -from pros.config import Config -from .depots import Depot, HttpDepot -from .project import Project -from .templates import BaseTemplate, ExternalTemplate, LocalTemplate, Template - -MAINLINE_NAME = 'pros-mainline' -MAINLINE_URL = 'https://pros.cs.purdue.edu/v5/_static/releases/pros-mainline.json' -EARLY_ACCESS_NAME = 'kernel-early-access-mainline' -EARLY_ACCESS_URL = 'https://pros.cs.purdue.edu/v5/_static/beta/beta-pros-mainline.json' - -""" -# TBD? Currently, EarlyAccess value is stored in config file -class ReleaseChannel(Enum): - Stable = 'stable' - Beta = 'beta' -""" - -def is_pathname_valid(pathname: str) -> bool: - ''' - A more detailed check for path validity than regex. - https://stackoverflow.com/a/34102855/11177720 - ''' - try: - if not isinstance(pathname, str) or not pathname: - return False - - _, pathname = os.path.splitdrive(pathname) - - root_dirname = os.environ.get('HOMEDRIVE', 'C:') \ - if sys.platform == 'win32' else os.path.sep - assert os.path.isdir(root_dirname) - - root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep - for pathname_part in pathname.split(os.path.sep): - try: - os.lstat(root_dirname + pathname_part) - except OSError as exc: - if hasattr(exc, 'winerror'): - if exc.winerror == 123: # ERROR_INVALID_NAME, python doesn't have this constant - return False - elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}: - return False - - # Check for emojis - # https://stackoverflow.com/a/62898106/11177720 - ranges = [ - (ord(u'\U0001F300'), ord(u"\U0001FAF6")), # 127744, 129782 - (126980, 127569), - (169, 174), - (8205, 12953) - ] - for a_char in pathname: - char_code = ord(a_char) - for range_min, range_max in ranges: - if range_min <= char_code <= range_max: - return False - except TypeError as exc: - return False - else: - return True - -class Conductor(Config): - """ - Provides entrances for all conductor-related tasks (fetching, applying, creating new projects) - """ - def __init__(self, file=None): - if not file: - file = os.path.join(click.get_app_dir('PROS'), 'conductor.pros') - self.local_templates: Set[LocalTemplate] = set() - self.early_access_local_templates: Set[LocalTemplate] = set() - self.depots: Dict[str, Depot] = {} - self.default_target: str = 'v5' - self.pros_3_default_libraries: Dict[str, List[str]] = None - self.pros_4_default_libraries: Dict[str, List[str]] = None - self.use_early_access = False - self.warn_early_access = False - super(Conductor, self).__init__(file) - needs_saving = False - if MAINLINE_NAME not in self.depots or \ - not isinstance(self.depots[MAINLINE_NAME], HttpDepot) or \ - self.depots[MAINLINE_NAME].location != MAINLINE_URL: - self.depots[MAINLINE_NAME] = HttpDepot(MAINLINE_NAME, MAINLINE_URL) - needs_saving = True - # add early access depot as another remote depot - if EARLY_ACCESS_NAME not in self.depots or \ - not isinstance(self.depots[EARLY_ACCESS_NAME], HttpDepot) or \ - self.depots[EARLY_ACCESS_NAME].location != EARLY_ACCESS_URL: - self.depots[EARLY_ACCESS_NAME] = HttpDepot(EARLY_ACCESS_NAME, EARLY_ACCESS_URL) - needs_saving = True - if self.default_target is None: - self.default_target = 'v5' - needs_saving = True - if self.pros_3_default_libraries is None: - self.pros_3_default_libraries = { - 'v5': ['okapilib'], - 'cortex': [] - } - needs_saving = True - if self.pros_4_default_libraries is None: - self.pros_4_default_libraries = { - 'v5': ['liblvgl'], - 'cortex': [] - } - needs_saving = True - if 'v5' not in self.pros_3_default_libraries: - self.pros_3_default_libraries['v5'] = ['okapilib'] - needs_saving = True - if 'cortex' not in self.pros_3_default_libraries: - self.pros_3_default_libraries['cortex'] = [] - needs_saving = True - if 'v5' not in self.pros_4_default_libraries: - self.pros_4_default_libraries['v5'] = ['liblvgl'] - needs_saving = True - if 'cortex' not in self.pros_4_default_libraries: - self.pros_4_default_libraries['cortex'] = [] - needs_saving = True - if needs_saving: - self.save() - from pros.common.sentry import add_context - add_context(self) - - def get_depot(self, name: str) -> Optional[Depot]: - return self.depots.get(name) - - def fetch_template(self, depot: Depot, template: BaseTemplate, **kwargs) -> LocalTemplate: - for t in list(self.local_templates): - if t.identifier == template.identifier: - self.purge_template(t) - - if 'destination' in kwargs: # this is deprecated, will work (maybe) but not desirable behavior - destination = kwargs.pop('destination') - else: - destination = os.path.join(self.directory, 'templates', template.identifier) - if os.path.isdir(destination): - shutil.rmtree(destination) - - template: Template = depot.fetch_template(template, destination, **kwargs) - click.secho(f'Fetched {template.identifier} from {depot.name} depot', dim=True) - local_template = LocalTemplate(orig=template, location=destination) - local_template.metadata['origin'] = depot.name - click.echo(f'Adding {local_template.identifier} to registry...', nl=False) - if depot.name == EARLY_ACCESS_NAME: # check for early access - self.early_access_local_templates.add(local_template) - else: - self.local_templates.add(local_template) - self.save() - if isinstance(template, ExternalTemplate) and template.directory == destination: - template.delete() - click.secho('Done', fg='green') - return local_template - - def purge_template(self, template: LocalTemplate): - if template.metadata['origin'] == EARLY_ACCESS_NAME: - if template not in self.early_access_local_templates: - logger(__name__).info(f"{template.identifier} was not in the Conductor's local early access templates cache.") - else: - self.early_access_local_templates.remove(template) - else: - if template not in self.local_templates: - logger(__name__).info(f"{template.identifier} was not in the Conductor's local templates cache.") - else: - self.local_templates.remove(template) - - if os.path.abspath(template.location).startswith( - os.path.abspath(os.path.join(self.directory, 'templates'))) \ - and os.path.isdir(template.location): - shutil.rmtree(template.location) - self.save() - - def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: bool = True, - allow_offline: bool = True, force_refresh: bool = False, - unique: bool = True, **kwargs) -> List[BaseTemplate]: - results = list() if not unique else set() - kernel_version = kwargs.get('kernel_version', None) - if kwargs.get('early_access', None) is not None: - use_early_access = kwargs.get('early_access', False) - else: - use_early_access = self.use_early_access - if isinstance(identifier, str): - query = BaseTemplate.create_query(name=identifier, **kwargs) - else: - query = identifier - if allow_offline: - offline_results = list() - - if use_early_access: - offline_results.extend(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.early_access_local_templates)) - - offline_results.extend(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.local_templates)) - - if unique: - results.update(offline_results) - else: - results.extend(offline_results) - if allow_online: - for depot in self.depots.values(): - # EarlyAccess depot will only be accessed when the --early-access flag is true - if depot.name != EARLY_ACCESS_NAME or (depot.name == EARLY_ACCESS_NAME and use_early_access): - remote_templates = depot.get_remote_templates(force_check=force_refresh, **kwargs) - online_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), - remote_templates)) - - if unique: - results.update(online_results) - else: - results.extend(online_results) - logger(__name__).debug('Saving Conductor config after checking for remote updates') - self.save() # Save self since there may have been some updates from the depots - - if len(results) == 0 and not use_early_access: - raise dont_send( - InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) - - return list(results) - - def resolve_template(self, identifier: Union[str, BaseTemplate], **kwargs) -> Optional[BaseTemplate]: - if isinstance(identifier, str): - kwargs['name'] = identifier - elif isinstance(identifier, BaseTemplate): - kwargs['orig'] = identifier - query = BaseTemplate.create_query(**kwargs) - logger(__name__).info(f'Query: {query}') - logger(__name__).debug(query.__dict__) - templates = self.resolve_templates(query, **kwargs) - logger(__name__).info(f'Candidates: {", ".join([str(t) for t in templates])}') - if not any(templates): - return None - query.version = str(Spec(query.version or '>0').select([Version(t.version) for t in templates])) - v = Version(query.version) - v.prerelease = v.prerelease if len(v.prerelease) else ('',) - v.build = v.build if len(v.build) else ('',) - query.version = f'=={v}' - logger(__name__).info(f'Resolved to {query.identifier}') - templates = self.resolve_templates(query, **kwargs) - if not any(templates): - return None - # prefer local templates first - local_templates = [t for t in templates if isinstance(t, LocalTemplate)] - if any(local_templates): - # there's a local template satisfying the query - if len(local_templates) > 1: - # This should never happen! Conductor state must be invalid - raise Exception(f'Multiple local templates satisfy {query.identifier}!') - return local_templates[0] - - # prefer pros-mainline template second - mainline_templates = [t for t in templates if t.metadata['origin'] == 'pros-mainline'] - if any(mainline_templates): - return mainline_templates[0] - - # No preference, just FCFS - return templates[0] - - def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], **kwargs): - upgrade_ok = kwargs.get('upgrade_ok', True) - install_ok = kwargs.get('install_ok', True) - downgrade_ok = kwargs.get('downgrade_ok', True) - download_ok = kwargs.get('download_ok', True) - force = kwargs.get('force_apply', False) - - kwargs['target'] = project.target - if 'kernel' in project.templates: - # support_kernels for backwards compatibility, but kernel_version should be getting most of the exposure - kwargs['kernel_version'] = kwargs['supported_kernels'] = project.templates['kernel'].version - template = self.resolve_template(identifier=identifier, allow_online=download_ok, **kwargs) - if template is None: - raise dont_send( - InvalidTemplateException(f'Could not find a template satisfying {identifier} for {project.target}')) - - apply_liblvgl = False # flag to apply liblvgl if upgrading to PROS 4 - - # warn and prompt user if upgrading to PROS 4 or downgrading to PROS 3 - if template.name == 'kernel': - isProject = Project.find_project("") - if isProject: - curr_proj = Project() - if curr_proj.kernel: - if template.version[0] == '4' and curr_proj.kernel[0] == '3': - confirm = ui.confirm(f'Warning! Upgrading project to PROS 4 will cause breaking changes. ' - f'Do you still want to upgrade?') - if not confirm: - raise dont_send( - InvalidTemplateException(f'Not upgrading')) - apply_liblvgl = True - if template.version[0] == '3' and curr_proj.kernel[0] == '4': - confirm = ui.confirm(f'Warning! Downgrading project to PROS 3 will cause breaking changes. ' - f'Do you still want to downgrade?') - if not confirm: - raise dont_send( - InvalidTemplateException(f'Not downgrading')) - - if not isinstance(template, LocalTemplate): - with ui.Notification(): - template = self.fetch_template(self.get_depot(template.metadata['origin']), template, **kwargs) - assert isinstance(template, LocalTemplate) - - logger(__name__).info(str(project)) - valid_action = project.get_template_actions(template) - if valid_action == TemplateAction.NotApplicable: - raise dont_send( - InvalidTemplateException(f'{template.identifier} is not applicable to {project}', reason=valid_action) - ) - if force \ - or (valid_action == TemplateAction.Upgradable and upgrade_ok) \ - or (valid_action == TemplateAction.Installable and install_ok) \ - or (valid_action == TemplateAction.Downgradable and downgrade_ok): - project.apply_template(template, force_system=kwargs.pop('force_system', False), - force_user=kwargs.pop('force_user', False), - remove_empty_directories=kwargs.pop('remove_empty_directories', False)) - ui.finalize('apply', f'Finished applying {template.identifier} to {project.location}') - - # Apply liblvgl if upgrading to PROS 4 - if apply_liblvgl: - template = self.resolve_template(identifier="liblvgl", allow_online=download_ok, early_access=True) - if not isinstance(template, LocalTemplate): - with ui.Notification(): - template = self.fetch_template(self.get_depot(template.metadata['origin']), template, **kwargs) - assert isinstance(template, LocalTemplate) - project.apply_template(template) - ui.finalize('apply', f'Finished applying {template.identifier} to {project.location}') - elif valid_action != TemplateAction.AlreadyInstalled: - raise dont_send( - InvalidTemplateException(f'Could not install {template.identifier} because it is {valid_action.name},' - f' and that is not allowed.', reason=valid_action) - ) - else: - ui.finalize('apply', f'{template.identifier} is already installed in {project.location}') - - @staticmethod - def remove_template(project: Project, identifier: Union[str, BaseTemplate], remove_user: bool = True, - remove_empty_directories: bool = True): - ui.logger(__name__).debug(f'Uninstalling templates matching {identifier}') - if not project.resolve_template(identifier): - ui.echo(f"{identifier} is not an applicable template") - for template in project.resolve_template(identifier): - ui.echo(f'Uninstalling {template.identifier}') - project.remove_template(template, remove_user=remove_user, - remove_empty_directories=remove_empty_directories) - - def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Project: - if kwargs.get('early_access', None) is not None: - use_early_access = kwargs.get('early_access', False) - else: - use_early_access = self.use_early_access - kwargs["early_access"] = use_early_access - if use_early_access: - ui.echo(f'Early access is enabled. Experimental features have been applied.') - - if not is_pathname_valid(str(Path(path).absolute())): - raise dont_send(ValueError('Project path contains invalid characters.')) - - if Path(path).exists() and Path(path).samefile(os.path.expanduser('~')): - raise dont_send(ValueError('Will not create a project in user home directory')) - - proj = Project(path=path, create=True, early_access=use_early_access) - if 'target' in kwargs: - proj.target = kwargs['target'] - if 'project_name' in kwargs and kwargs['project_name'] and not kwargs['project_name'].isspace(): - proj.project_name = kwargs['project_name'] - else: - proj.project_name = os.path.basename(os.path.normpath(os.path.abspath(path))) - if 'version' in kwargs: - if kwargs['version'] == 'latest': - kwargs['version'] = '>=0' - self.apply_template(proj, identifier='kernel', **kwargs) - proj.save() - - if not no_default_libs: - major_version = proj.kernel[0] - libraries = self.pros_4_default_libraries if major_version == '4' else self.pros_3_default_libraries - for library in libraries[proj.target]: - try: - # remove kernel version so that latest template satisfying query is correctly selected - if 'version' in kwargs: - kwargs.pop('version') - self.apply_template(proj, library, **kwargs) - except Exception as e: - logger(__name__).exception(e) - return proj - - def add_depot(self, name: str, url: str): - self.depots[name] = HttpDepot(name, url) - self.save() - - def remove_depot(self, name: str): - del self.depots[name] - self.save() - - def query_depots(self, url: bool): - return [name + ((' -- ' + depot.location) if url else '') for name, depot in self.depots.items()] diff --git a/pros/conductor/depots.md b/pros/conductor/depots.md deleted file mode 100644 index 33a92336..00000000 --- a/pros/conductor/depots.md +++ /dev/null @@ -1,45 +0,0 @@ -# Adding Depots - -`pros conduct add-depot ` - -Example: -```bash -$ pros conduct add-depot test "https://pros.cs.purdue.edu/v5/_static/beta/testing-mainline.json" -> Added depot test from https://pros.cs.purdue.edu/v5/_static/beta/testing-mainline.json -``` - -# Removing Depots - -`pros conduct remove-depot ` - -Example: -```bash -$ pros conduct remove-depot test -> Removed depot test -``` - - -# Query Depots - -`pros conduct query-depots --url` -`pros conduct query-depots` - -Examples: -```bash -$ pros conduct query-depots --url -> Available Depots: -> -> kernel-beta-mainline -- https://raw.githubusercontent.com/purduesigbots/pros-mainline/master/beta/kernel-beta-mainline.json -> pros-mainline -- https://purduesigbots.github.io/pros-mainline/pros-mainline.json -> test -- https://pros.cs.purdue.edu/v5/_static/beta/testing-mainline.json -> -``` -```bash -$ pros conduct query-depots -> Available Depots (Add --url for the url): -> -> kernel-beta-mainline -> pros-mainline -> test -> -``` diff --git a/pros/conductor/depots/__init__.py b/pros/conductor/depots/__init__.py deleted file mode 100644 index 249cdd47..00000000 --- a/pros/conductor/depots/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .depot import Depot -from .http_depot import HttpDepot -from .local_depot import LocalDepot diff --git a/pros/conductor/depots/depot.py b/pros/conductor/depots/depot.py deleted file mode 100644 index 364d312f..00000000 --- a/pros/conductor/depots/depot.py +++ /dev/null @@ -1,40 +0,0 @@ -from datetime import datetime, timedelta -from typing import * - -import pros.common.ui as ui -from pros.common import logger -from pros.config.cli_config import cli_config -from ..templates import BaseTemplate, Template - - -class Depot(object): - def __init__(self, name: str, location: str, config: Dict[str, Any] = None, - update_frequency: timedelta = timedelta(minutes=1), - config_schema: Dict[str, Dict[str, Any]] = None): - self.name: str = name - self.location: str = location - self.config: Dict[str, Any] = config or {} - self.config_schema: Dict[str, Dict[str, Any]] = config_schema or {} - self.remote_templates: List[BaseTemplate] = [] - self.last_remote_update: datetime = datetime(2000, 1, 1) # long enough time ago to force re-check - self.update_frequency: timedelta = update_frequency - - def update_remote_templates(self, **_): - self.last_remote_update = datetime.now() - - def fetch_template(self, template: BaseTemplate, destination: str, **kwargs) -> Template: - raise NotImplementedError() - - def get_remote_templates(self, auto_check_freq: Optional[timedelta] = None, force_check: bool = False, **kwargs): - if auto_check_freq is None: - auto_check_freq = getattr(self, 'update_frequency', cli_config().update_frequency) - logger(__name__).info(f'Last check of {self.name} was {self.last_remote_update} ' - f'({datetime.now() - self.last_remote_update} vs {auto_check_freq}).') - if force_check or datetime.now() - self.last_remote_update > auto_check_freq: - with ui.Notification(): - ui.echo(f'Updating {self.name}... ', nl=False) - self.update_remote_templates(**kwargs) - ui.echo('Done', color='green') - for t in self.remote_templates: - t.metadata['origin'] = self.name - return self.remote_templates diff --git a/pros/conductor/depots/http_depot.py b/pros/conductor/depots/http_depot.py deleted file mode 100644 index dc7e3a25..00000000 --- a/pros/conductor/depots/http_depot.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import zipfile -from datetime import datetime, timedelta - -import jsonpickle - -import pros.common.ui as ui -from pros.common import logger -from pros.common.utils import download_file -from .depot import Depot -from ..templates import BaseTemplate, ExternalTemplate - - -class HttpDepot(Depot): - def __init__(self, name: str, location: str): - # Note: If update_frequency = timedelta(minutes=1) isn't included as a parameter, - # the beta depot won't be saved in conductor.json correctly - super().__init__(name, location, config_schema={}, update_frequency = timedelta(minutes=1)) - - def fetch_template(self, template: BaseTemplate, destination: str, **kwargs): - import requests - assert 'location' in template.metadata - url = template.metadata['location'] - tf = download_file(url, ext='zip', desc=f'Downloading {template.identifier}') - if tf is None: - raise requests.ConnectionError(f'Could not obtain {url}') - with zipfile.ZipFile(tf) as zf: - with ui.progressbar(length=len(zf.namelist()), - label=f'Extracting {template.identifier}') as pb: - for file in zf.namelist(): - zf.extract(file, path=destination) - pb.update(1) - os.remove(tf) - return ExternalTemplate(file=os.path.join(destination, 'template.pros')) - - def update_remote_templates(self, **_): - import requests - response = requests.get(self.location) - if response.status_code == 200: - self.remote_templates = jsonpickle.decode(response.text) - else: - logger(__name__).warning(f'Unable to access {self.name} ({self.location}): {response.status_code}') - self.last_remote_update = datetime.now() diff --git a/pros/conductor/depots/local_depot.py b/pros/conductor/depots/local_depot.py deleted file mode 100644 index 60bff121..00000000 --- a/pros/conductor/depots/local_depot.py +++ /dev/null @@ -1,52 +0,0 @@ -import os.path -import shutil -import zipfile - -import click - -from pros.config import ConfigNotFoundException -from .depot import Depot -from ..templates import BaseTemplate, Template, ExternalTemplate -from pros.common.utils import logger - - -class LocalDepot(Depot): - def fetch_template(self, template: BaseTemplate, destination: str, **kwargs) -> Template: - if 'location' not in kwargs: - logger(__name__).debug(f"Template not specified. Provided arguments: {kwargs}") - raise KeyError('Location of local template must be specified.') - location = kwargs['location'] - if os.path.isdir(location): - location_dir = location - if not os.path.isfile(os.path.join(location_dir, 'template.pros')): - raise ConfigNotFoundException(f'A template.pros file was not found in {location_dir}.') - template_file = os.path.join(location_dir, 'template.pros') - elif zipfile.is_zipfile(location): - with zipfile.ZipFile(location) as zf: - with click.progressbar(length=len(zf.namelist()), - label=f"Extracting {location}") as progress_bar: - for file in zf.namelist(): - zf.extract(file, path=destination) - progress_bar.update(1) - template_file = os.path.join(destination, 'template.pros') - location_dir = destination - elif os.path.isfile(location): - location_dir = os.path.dirname(location) - template_file = location - elif isinstance(template, ExternalTemplate): - location_dir = template.directory - template_file = template.save_file - else: - raise ValueError(f"The specified location was not a file or directory ({location}).") - if location_dir != destination: - n_files = len([os.path.join(dp, f) for dp, dn, fn in os.walk(location_dir) for f in fn]) - with click.progressbar(length=n_files, label='Copying to local cache') as pb: - def my_copy(*args): - pb.update(1) - shutil.copy2(*args) - - shutil.copytree(location_dir, destination, copy_function=my_copy) - return ExternalTemplate(file=template_file) - - def __init__(self): - super().__init__('local', 'local') diff --git a/pros/conductor/interactive/NewProjectModal.py b/pros/conductor/interactive/NewProjectModal.py deleted file mode 100644 index 9f71c76d..00000000 --- a/pros/conductor/interactive/NewProjectModal.py +++ /dev/null @@ -1,73 +0,0 @@ -import os.path -from typing import * - -from click import Context, get_current_context - -from pros.common import ui -from pros.common.ui.interactive import application, components, parameters -from pros.conductor import Conductor -from .parameters import NonExistentProjectParameter - - -class NewProjectModal(application.Modal[None]): - targets = parameters.OptionParameter('v5', ['v5', 'cortex']) - kernel_versions = parameters.OptionParameter('latest', ['latest']) - install_default_libraries = parameters.BooleanParameter(True) - - project_name = parameters.Parameter(None) - advanced_collapsed = parameters.BooleanParameter(True) - - def __init__(self, ctx: Context = None, conductor: Optional[Conductor] = None, - directory=os.path.join(os.path.expanduser('~'), 'My PROS Project')): - super().__init__('Create a new project') - self.conductor = conductor or Conductor() - self.click_ctx = ctx or get_current_context() - self.directory = NonExistentProjectParameter(directory) - - cb = self.targets.on_changed(self.target_changed, asynchronous=True) - cb(self.targets) - - def target_changed(self, new_target): - templates = self.conductor.resolve_templates('kernel', target=new_target.value) - if len(templates) == 0: - self.kernel_versions.options = ['latest'] - else: - self.kernel_versions.options = ['latest'] + sorted({t.version for t in templates}, reverse=True) - self.redraw() - - def confirm(self, *args, **kwargs): - assert self.can_confirm - self.exit() - project = self.conductor.new_project( - path=self.directory.value, - target=self.targets.value, - version=self.kernel_versions.value, - no_default_libs=not self.install_default_libraries.value, - project_name=self.project_name.value - ) - - from pros.conductor.project import ProjectReport - report = ProjectReport(project) - ui.finalize('project-report', report) - - with ui.Notification(): - ui.echo('Building project...') - project.compile([]) - - @property - def can_confirm(self): - return self.directory.is_valid() and self.targets.is_valid() and self.kernel_versions.is_valid() - - def build(self) -> Generator[components.Component, None, None]: - yield components.DirectorySelector('Project Directory', self.directory) - yield components.ButtonGroup('Target', self.targets) - - project_name_placeholder = os.path.basename(os.path.normpath(os.path.abspath(self.directory.value))) - - yield components.Container( - components.InputBox('Project Name', self.project_name, placeholder=project_name_placeholder), - components.DropDownBox('Kernel Version', self.kernel_versions), - components.Checkbox('Install default libraries', self.install_default_libraries), - title='Advanced', - collapsed=self.advanced_collapsed - ) diff --git a/pros/conductor/interactive/UpdateProjectModal.py b/pros/conductor/interactive/UpdateProjectModal.py deleted file mode 100644 index 9cb5124e..00000000 --- a/pros/conductor/interactive/UpdateProjectModal.py +++ /dev/null @@ -1,147 +0,0 @@ -import os.path -from typing import * - -from click import Context, get_current_context -from semantic_version import Version - -from pros.common import ui -from pros.common.ui.interactive import application, components, parameters -from pros.conductor import BaseTemplate, Conductor, Project -from pros.conductor.project.ProjectTransaction import ProjectTransaction -from .components import TemplateListingComponent -from .parameters import ExistingProjectParameter, TemplateParameter - - -class UpdateProjectModal(application.Modal[None]): - - @property - def is_processing(self): - return self._is_processing - - @is_processing.setter - def is_processing(self, value: bool): - self._is_processing = bool(value) - self.redraw() - - def _generate_transaction(self) -> ProjectTransaction: - transaction = ProjectTransaction(self.project, self.conductor) - apply_kwargs = dict( - force_apply=self.force_apply_parameter.value - ) - if self.name.value != self.project.name: - transaction.change_name(self.name.value) - if self.project.template_is_applicable(self.current_kernel.value, **apply_kwargs): - transaction.apply_template(self.current_kernel.value, **apply_kwargs) - for template in self.current_templates: - if template.removed: - transaction.rm_template(BaseTemplate.create_query(template.value.name)) - elif self.project.template_is_applicable(template.value, **apply_kwargs): - transaction.apply_template(template.value, **apply_kwargs) - for template in self.new_templates: - if not template.removed: # template should never be "removed" - transaction.apply_template(template.value, force_apply=self.force_apply_parameter.value) - return transaction - - def _add_template(self): - options = self.conductor.resolve_templates(identifier=BaseTemplate(target=self.project.target), unique=True) - ui.logger(__name__).debug(options) - p = TemplateParameter(None, options) - - @p.on('removed') - def remove_template(): - self.new_templates.remove(p) - - self.new_templates.append(p) - - def __init__(self, ctx: Optional[Context] = None, conductor: Optional[Conductor] = None, - project: Optional[Project] = None): - super().__init__('Update a project') - self.conductor = conductor or Conductor() - self.click_ctx = ctx or get_current_context() - self._is_processing = False - - self.project: Optional[Project] = project - self.project_path = ExistingProjectParameter( - str(project.location) if project else os.path.join(os.path.expanduser('~'), 'My PROS Project') - ) - - self.name = parameters.Parameter(None) - self.current_kernel: TemplateParameter = None - self.current_templates: List[TemplateParameter] = [] - self.new_templates: List[TemplateParameter] = [] - self.force_apply_parameter = parameters.BooleanParameter(False) - - self.templates_collapsed = parameters.BooleanParameter(False) - self.advanced_collapsed = parameters.BooleanParameter(True) - - self.add_template_button = components.Button('Add Template') - - self.add_template_button.on_clicked(self._add_template) - - cb = self.project_path.on_changed(self.project_changed, asynchronous=True) - if self.project_path.is_valid(): - cb(self.project_path) - - def project_changed(self, new_project: ExistingProjectParameter): - try: - self.is_processing = True - self.project = Project(new_project.value) - - self.name.update(self.project.project_name) - - self.current_kernel = TemplateParameter( - None, - options=sorted( - {t for t in self.conductor.resolve_templates(self.project.templates['kernel'].as_query())}, - key=lambda v: Version(v.version), reverse=True - ) - ) - self.current_templates = [ - TemplateParameter( - None, - options=sorted({ - t - for t in self.conductor.resolve_templates(t.as_query()) - }, key=lambda v: Version(v.version), reverse=True) - ) - for t in self.project.templates.values() - if t.name != 'kernel' - ] - self.new_templates = [] - - self.is_processing = False - except BaseException as e: - ui.logger(__name__).exception(e) - - def confirm(self, *args, **kwargs): - self.exit() - self._generate_transaction().execute() - - @property - def can_confirm(self): - return self.project and self._generate_transaction().can_execute() - - def build(self) -> Generator[components.Component, None, None]: - yield components.DirectorySelector('Project Directory', self.project_path) - if self.is_processing: - yield components.Spinner() - elif self.project_path.is_valid(): - assert self.project is not None - yield components.Label(f'Modify your {self.project.target} project.') - yield components.InputBox('Project Name', self.name) - yield TemplateListingComponent(self.current_kernel, editable=dict(version=True), removable=False) - yield components.Container( - *(TemplateListingComponent(t, editable=dict(version=True), removable=True) for t in - self.current_templates), - *(TemplateListingComponent(t, editable=True, removable=True) for t in self.new_templates), - self.add_template_button, - title='Templates', - collapsed=self.templates_collapsed - ) - yield components.Container( - components.Checkbox('Re-apply all templates', self.force_apply_parameter), - title='Advanced', - collapsed=self.advanced_collapsed - ) - yield components.Label('What will happen when you click "Continue":') - yield components.VerbatimLabel(self._generate_transaction().describe()) diff --git a/pros/conductor/interactive/__init__.py b/pros/conductor/interactive/__init__.py deleted file mode 100644 index 89f1e51c..00000000 --- a/pros/conductor/interactive/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .NewProjectModal import NewProjectModal -from .UpdateProjectModal import UpdateProjectModal - -from .parameters import ExistingProjectParameter, NonExistentProjectParameter diff --git a/pros/conductor/interactive/components.py b/pros/conductor/interactive/components.py deleted file mode 100644 index b5bfacb7..00000000 --- a/pros/conductor/interactive/components.py +++ /dev/null @@ -1,41 +0,0 @@ -from collections import defaultdict -from typing import * - -from pros.common.ui.interactive import components, parameters -from pros.conductor.interactive.parameters import TemplateParameter - - -class TemplateListingComponent(components.Container): - def _generate_components(self) -> Generator[components.Component, None, None]: - if not self.editable['name'] and not self.editable['version']: - yield components.Label(self.template.value.identifier) - else: - if self.editable['name']: - yield components.InputBox('Name', self.template.name) - else: - yield components.Label(self.template.value.name) - if self.editable['version']: - if isinstance(self.template.version, parameters.OptionParameter): - yield components.DropDownBox('Version', self.template.version) - else: - yield components.InputBox('Version', self.template.version) - else: - yield components.Label(self.template.value.version) - if self.removable: - remove_button = components.Button('Don\'t remove' if self.template.removed else 'Remove') - remove_button.on_clicked(lambda: self.template.trigger('removed')) - yield remove_button - - def __init__(self, template: TemplateParameter, - removable: bool = False, - editable: Union[Dict[str, bool], bool] = True): - self.template = template - self.removable = removable - if isinstance(editable, bool): - self.editable = defaultdict(lambda: editable) - else: - self.editable = defaultdict(lambda: False) - if isinstance(editable, dict): - self.editable.update(**editable) - - super().__init__(*self._generate_components()) diff --git a/pros/conductor/interactive/parameters.py b/pros/conductor/interactive/parameters.py deleted file mode 100644 index 7b0da738..00000000 --- a/pros/conductor/interactive/parameters.py +++ /dev/null @@ -1,126 +0,0 @@ -import os.path -import sys -from pathlib import Path -from typing import * - -from semantic_version import Spec, Version - -from pros.common.ui.interactive import parameters as p -from pros.conductor import BaseTemplate, Project - - -class NonExistentProjectParameter(p.ValidatableParameter[str]): - def validate(self, value: str) -> Union[bool, str]: - value = os.path.abspath(value) - if os.path.isfile(value): - return 'Path is a file' - if os.path.isdir(value) and not os.access(value, os.W_OK): - return 'Do not have write permission to path' - if Project.find_project(value) is not None: - return 'Project path already exists, delete it first' - blacklisted_directories = [] - # TODO: Proper Windows support - if sys.platform == 'win32': - blacklisted_directories.extend([ - os.environ.get('WINDIR', os.path.join('C:', 'Windows')), - os.environ.get('PROGRAMFILES', os.path.join('C:', 'Program Files')) - ]) - if any(value.startswith(d) for d in blacklisted_directories): - return 'Cannot create project in a system directory' - if Path(value).exists() and Path(value).samefile(os.path.expanduser('~')): - return 'Should not create a project in home directory' - if not os.path.exists(value): - parent = os.path.split(value)[0] - while parent and not os.path.exists(parent): - temp_value = os.path.split(parent)[0] - if parent == temp_value: - break - parent = temp_value - if not parent: - return 'Cannot create directory because root does not exist' - if not os.path.exists(parent): - return f'Cannot create directory because {parent} does not exist' - if not os.path.isdir(parent): - return f'Cannot create directory because {parent} is a file' - if not os.access(parent, os.W_OK | os.X_OK): - return f'Cannot create directory because missing write permissions to {parent}' - return True - - -class ExistingProjectParameter(p.ValidatableParameter[str]): - def update(self, new_value): - project = Project.find_project(new_value) - if project: - project = Project(project).directory - super(ExistingProjectParameter, self).update(project or new_value) - - def validate(self, value: str): - project = Project.find_project(value) - return project is not None or 'Path is not inside a PROS project' - - -class TemplateParameter(p.ValidatableParameter[BaseTemplate]): - def _update_versions(self): - if self.name.value in self.options: - self.version = p.OptionParameter( - self.version.value if self.version else None, - list(sorted(self.options[self.name.value].keys(), reverse=True, key=lambda v: Version(v))) - ) - - if self.version.value not in self.version.options: - self.version.value = self.version.options[0] - - self.value = self.options[self.name.value][self.version.value] - self.trigger('changed_validated', self) - else: - self.version = p.AlwaysInvalidParameter(self.value.version) - - def __init__(self, template: Optional[BaseTemplate], options: List[BaseTemplate], allow_invalid_input: bool = True): - if not template and len(options) == 0: - raise ValueError('At least template or versions must be defined for a TemplateParameter') - - self.options = {t.name: {_t.version: _t for _t in options if t.name == _t.name} for t in options} - - if not template: - first_template = list(self.options.values())[0] - template = first_template[str(Spec('>0').select([Version(v) for v in first_template.keys()]))] - - super().__init__(template, allow_invalid_input) - - self.name: p.ValidatableParameter[str] = p.ValidatableParameter( - self.value.name, - allow_invalid_input, - validate=lambda v: True if v in self.options.keys() else f'Could not find a template named {v}' - ) - if not self.value.version and self.value.name in self.options: - self.value.version = Spec('>0').select([Version(v) for v in self.options[self.value.name].keys()]) - - self.version = None - self._update_versions() - - @self.name.on_any_changed - def name_any_changed(v: p.ValidatableParameter): - self._update_versions() - self.trigger('changed', self) - - @self.version.on_any_changed - def version_any_changed(v: p.ValidatableParameter): - if v.value in self.options[self.name.value].keys(): - self.value = self.options[self.name.value][v.value] - self.trigger('changed_validated', self) - else: - self.value.version = v.value - self.trigger('changed', self) - - # self.name.on_changed(lambda v: self.trigger('changed_validated', self)) - # self.version.on_changed(lambda v: self.trigger('changed_validated', self)) - - self.removed = False - - @self.on('removed') - def removed_changed(): - self.removed = not self.removed - - def is_valid(self, value: BaseTemplate = None): - return self.name.is_valid(value.name if value else None) and \ - self.version.is_valid(value.version if value else None) diff --git a/pros/conductor/project/ProjectReport.py b/pros/conductor/project/ProjectReport.py deleted file mode 100644 index 75d2ff3a..00000000 --- a/pros/conductor/project/ProjectReport.py +++ /dev/null @@ -1,25 +0,0 @@ -import os.path - - -class ProjectReport(object): - def __init__(self, project: 'Project'): - self.project = { - "target": project.target, - "location": os.path.abspath(project.location), - "name": project.name, - "templates": [{"name": t.name, "version": t.version, "origin": t.origin} for t in - project.templates.values()] - } - - def __str__(self): - import tabulate - s = f'PROS Project for {self.project["target"]} at: {self.project["location"]}' \ - f' ({self.project["name"]})' if self.project["name"] else '' - s += '\n' - rows = [t.values() for t in self.project["templates"]] - headers = [h.capitalize() for h in self.project["templates"][0].keys()] - s += tabulate.tabulate(rows, headers=headers) - return s - - def __getstate__(self): - return self.__dict__ diff --git a/pros/conductor/project/ProjectTransaction.py b/pros/conductor/project/ProjectTransaction.py deleted file mode 100644 index 14034d42..00000000 --- a/pros/conductor/project/ProjectTransaction.py +++ /dev/null @@ -1,174 +0,0 @@ -import itertools as it -import os -import tempfile -import zipfile -from typing import * - -import pros.common.ui as ui -import pros.conductor as c -from pros.conductor.project.template_resolution import InvalidTemplateException, TemplateAction - - -class Action(object): - def execute(self, conductor: c.Conductor, project: c.Project) -> None: - raise NotImplementedError() - - def describe(self, conductor: c.Conductor, project: c.Project) -> str: - raise NotImplementedError() - - def can_execute(self, conductor: c.Conductor, project: c.Project) -> bool: - raise NotImplementedError() - - -class ApplyTemplateAction(Action): - - def __init__(self, template: c.BaseTemplate, apply_kwargs: Dict[str, Any] = None, - suppress_already_installed: bool = False): - self.template = template - self.apply_kwargs = apply_kwargs or {} - self.suppress_already_installed = suppress_already_installed - - def execute(self, conductor: c.Conductor, project: c.Project): - try: - conductor.apply_template(project, self.template, **self.apply_kwargs) - except InvalidTemplateException as e: - if e.reason != TemplateAction.AlreadyInstalled or not self.suppress_already_installed: - raise e - else: - ui.logger(__name__).warning(str(e)) - return None - - def describe(self, conductor: c.Conductor, project: c.Project): - action = project.get_template_actions(conductor.resolve_template(self.template)) - if action == TemplateAction.NotApplicable: - return f'{self.template.identifier} cannot be applied to project!' - if action == TemplateAction.Installable: - return f'{self.template.identifier} will installed to project.' - if action == TemplateAction.Downgradable: - return f'Project will be downgraded to {self.template.identifier} from' \ - f' {project.templates[self.template.name].version}.' - if action == TemplateAction.Upgradable: - return f'Project will be upgraded to {self.template.identifier} from' \ - f' {project.templates[self.template.name].version}.' - if action == TemplateAction.AlreadyInstalled: - if self.apply_kwargs.get('force_apply'): - return f'{self.template.identifier} will be re-applied.' - elif self.suppress_already_installed: - return f'{self.template.identifier} will not be re-applied.' - else: - return f'{self.template.identifier} cannot be applied to project because it is already installed.' - - def can_execute(self, conductor: c.Conductor, project: c.Project) -> bool: - action = project.get_template_actions(conductor.resolve_template(self.template)) - if action == TemplateAction.AlreadyInstalled: - return self.apply_kwargs.get('force_apply') or self.suppress_already_installed - return action in [TemplateAction.Installable, TemplateAction.Downgradable, TemplateAction.Upgradable] - - -class RemoveTemplateAction(Action): - def __init__(self, template: c.BaseTemplate, remove_kwargs: Dict[str, Any] = None, - suppress_not_removable: bool = False): - self.template = template - self.remove_kwargs = remove_kwargs or {} - self.suppress_not_removable = suppress_not_removable - - def execute(self, conductor: c.Conductor, project: c.Project): - try: - conductor.remove_template(project, self.template, **self.remove_kwargs) - except ValueError as e: - if not self.suppress_not_removable: - raise e - else: - ui.logger(__name__).warning(str(e)) - - def describe(self, conductor: c.Conductor, project: c.Project) -> str: - return f'{self.template.identifier} will be removed' - - def can_execute(self, conductor: c.Conductor, project: c.Project): - return True - - -class ChangeProjectNameAction(Action): - def __init__(self, new_name: str): - self.new_name = new_name - - def execute(self, conductor: c.Conductor, project: c.Project): - project.project_name = self.new_name - project.save() - - def describe(self, conductor: c.Conductor, project: c.Project): - return f'Project will be renamed to: "{self.new_name}"' - - def can_execute(self, conductor: c.Conductor, project: c.Project): - return True - - -class ProjectTransaction(object): - def __init__(self, project: c.Project, conductor: Optional[c.Conductor] = None): - self.project = project - self.conductor = conductor or c.Conductor() - self.actions: List[Action] = [] - - def add_action(self, action: Action) -> None: - self.actions.append(action) - - def execute(self): - if len(self.actions) == 0: - ui.logger(__name__).warning('No actions necessary.') - return - location = self.project.location - tfd, tfn = tempfile.mkstemp(prefix='pros-project-', suffix=f'-{self.project.name}.zip', text='w+b') - with os.fdopen(tfd, 'w+b') as tf: - with zipfile.ZipFile(tf, mode='w') as zf: - files, length = it.tee(location.glob('**/*'), 2) - length = len(list(length)) - with ui.progressbar(files, length=length, label=f'Backing up {self.project.name} to {tfn}') as pb: - for file in pb: - zf.write(file, arcname=file.relative_to(location)) - - try: - with ui.Notification(): - for action in self.actions: - ui.logger(__name__).debug(action.describe(self.conductor, self.project)) - rv = action.execute(self.conductor, self.project) - ui.logger(__name__).debug(f'{action} returned {rv}') - if rv is not None and not rv: - raise ValueError('Action did not complete successfully') - ui.echo('All actions performed successfully') - except Exception as e: - ui.logger(__name__).warning(f'Failed to perform transaction, restoring project to previous state') - - with zipfile.ZipFile(tfn) as zf: - with ui.progressbar(zf.namelist(), label=f'Restoring {self.project.name} from {tfn}') as pb: - for file in pb: - zf.extract(file, path=location) - - ui.logger(__name__).exception(e) - finally: - ui.echo(f'Removing {tfn}') - os.remove(tfn) - - def apply_template(self, template: c.BaseTemplate, suppress_already_installed: bool = False, **kwargs): - self.add_action( - ApplyTemplateAction(template, suppress_already_installed=suppress_already_installed, apply_kwargs=kwargs) - ) - - def rm_template(self, template: c.BaseTemplate, suppress_not_removable: bool = False, **kwargs): - self.add_action( - RemoveTemplateAction(template, suppress_not_removable=suppress_not_removable, remove_kwargs=kwargs) - ) - - def change_name(self, new_name: str): - self.add_action(ChangeProjectNameAction(new_name)) - - def describe(self) -> str: - if len(self.actions) > 0: - return '\n'.join( - f'- {a.describe(self.conductor, self.project)}' - for a in self.actions - ) - else: - return 'No actions necessary.' - - def can_execute(self) -> bool: - return all(a.can_execute(self.conductor, self.project) for a in self.actions) diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py deleted file mode 100644 index 575f9c4d..00000000 --- a/pros/conductor/project/__init__.py +++ /dev/null @@ -1,428 +0,0 @@ -import glob -import io -import os.path -import pathlib -import sys -from pathlib import Path -from typing import * - -from pros.common import * -from pros.common.ui import EchoPipe -from pros.conductor.project.template_resolution import TemplateAction -from pros.config.config import Config, ConfigNotFoundException -from .ProjectReport import ProjectReport -from ..templates import BaseTemplate, LocalTemplate, Template -from ..transaction import Transaction - - -class Project(Config): - def __init__(self, path: str = '.', create: bool = False, raise_on_error: bool = True, defaults: dict = None, early_access: bool = False): - """ - Instantiates a PROS project configuration - :param path: A path to the project, may be the actual project.pros file, any child directory of the project, - or the project directory itself. See Project.find_project for more details - :param create: The default implementation of this initializer is to raise a ConfigNotFoundException if the - project was not found. Create allows - :param raise_on_error: - :param defaults: - """ - file = Project.find_project(path or '.') - if file is None and create: - file = os.path.join(path, 'project.pros') if not os.path.basename(path) == 'project.pros' else path - elif file is None and raise_on_error: - raise ConfigNotFoundException('A project config was not found for {}'.format(path)) - - if defaults is None: - defaults = {} - self.target: str = defaults.get('target', 'v5').lower() # VEX Hardware target (V5/Cortex) - self.templates: Dict[str, Template] = defaults.get('templates', {}) - self.upload_options: Dict = defaults.get('upload_options', {}) - self.project_name: str = defaults.get('project_name', None) - self.use_early_access = early_access - super(Project, self).__init__(file, error_on_decode=raise_on_error) - if 'kernel' in self.__dict__: - # Add backwards compatibility with PROS CLI 2 projects by adding kernel as a pseudo-template - self.templates['kernel'] = Template(user_files=self.all_files, name='kernel', - version=self.__dict__['kernel'], target=self.target, - output='bin/output.bin') - - @property - def location(self) -> pathlib.Path: - return pathlib.Path(os.path.dirname(self.save_file)) - - @property - def path(self): - return Path(self.location) - - @property - def name(self): - return self.project_name or os.path.basename(self.location) \ - or os.path.basename(self.templates['kernel'].metadata['output']) \ - or 'pros' - - @property - def all_files(self) -> Set[str]: - return {os.path.relpath(p, self.location) for p in - glob.glob(f'{self.location}/**/*', recursive=True)} - - def get_template_actions(self, template: BaseTemplate) -> TemplateAction: - ui.logger(__name__).debug(template) - if template.target != self.target: - return TemplateAction.NotApplicable - from semantic_version import Spec, Version - if template.name != 'kernel' and Version(self.kernel) not in Spec(template.supported_kernels or '>0'): - if template.name in self.templates.keys(): - return TemplateAction.AlreadyInstalled - return TemplateAction.NotApplicable - for current in self.templates.values(): - if template.name != current.name: - continue - if template > current: - return TemplateAction.Upgradable - if template == current: - return TemplateAction.AlreadyInstalled - if current > template: - return TemplateAction.Downgradable - - if any([template > current for current in self.templates.values()]): - return TemplateAction.Upgradable - else: - return TemplateAction.Installable - - def template_is_installed(self, query: BaseTemplate) -> bool: - return self.get_template_actions(query) == TemplateAction.AlreadyInstalled - - def template_is_upgradeable(self, query: BaseTemplate) -> bool: - return self.get_template_actions(query) == TemplateAction.Upgradable - - def template_is_applicable(self, query: BaseTemplate, force_apply: bool = False) -> bool: - ui.logger(__name__).debug(query.target) - return self.get_template_actions(query) in ( - TemplateAction.ForcedApplicable if force_apply else TemplateAction.UnforcedApplicable) - - def apply_template(self, template: LocalTemplate, force_system: bool = False, force_user: bool = False, - remove_empty_directories: bool = False): - """ - Applies a template to a project - :param remove_empty_directories: - :param template: - :param force_system: - :param force_user: - :return: - """ - assert template.target == self.target - transaction = Transaction(self.location, set(self.all_files)) - installed_user_files = set() - for lib_name, lib in self.templates.items(): - if lib_name == template.name or lib.name == template.name: - logger(__name__).debug(f'{lib} is already installed') - logger(__name__).debug(lib.system_files) - logger(__name__).debug(lib.user_files) - transaction.extend_rm(lib.system_files) - installed_user_files = installed_user_files.union(lib.user_files) - if force_user: - transaction.extend_rm(lib.user_files) - - # remove newly deprecated user files - deprecated_user_files = installed_user_files.intersection(self.all_files) - set(template.user_files) - if any(deprecated_user_files): - if force_user or confirm(f'The following user files have been deprecated: {deprecated_user_files}. ' - f'Do you want to update them?'): - transaction.extend_rm(deprecated_user_files) - else: - logger(__name__).warning(f'Deprecated user files may cause weird quirks. See migration guidelines from ' - f'{template.identifier}\'s release notes.') - # Carry forward deprecated user files into the template about to be applied so that user gets warned in - # future. - template.user_files.extend(deprecated_user_files) - - def new_user_filter(new_file: str) -> bool: - """ - Filter new user files that do not have an existing friend present in the project - - Friend files are files which have the same basename - src/opcontrol.c and src/opcontrol.cpp are friends because they have the same stem - src/opcontrol.c and include/opcontrol.h are not because they are in different directories - """ - return not any([(os.path.normpath(file) in transaction.effective_state) for file in template.user_files if - os.path.splitext(file)[0] == os.path.splitext(new_file)[0]]) - - if force_user: - new_user_files = template.real_user_files - else: - new_user_files = filter(new_user_filter, template.real_user_files) - transaction.extend_add(new_user_files, template.location) - - if any([file in transaction.effective_state for file in template.system_files]) and not force_system: - confirm(f'Some required files for {template.identifier} already exist in the project. ' - f'Overwrite the existing files?', abort=True) - transaction.extend_add(template.system_files, template.location) - - logger(__name__).debug(transaction) - transaction.commit(label=f'Applying {template.identifier}', remove_empty_directories=remove_empty_directories) - self.templates[template.name] = template - self.save() - - def remove_template(self, template: Template, remove_user: bool = False, remove_empty_directories: bool = True): - if not self.template_is_installed(template): - raise ValueError(f'{template.identifier} is not installed on this project.') - if template.name == 'kernel': - raise ValueError(f'Cannot remove the kernel template. Maybe create a new project?') - - real_template = LocalTemplate(orig=template, location=self.location) - transaction = Transaction(self.location, set(self.all_files)) - transaction.extend_rm(real_template.real_system_files) - if remove_user: - transaction.extend_rm(real_template.real_user_files) - logger(__name__).debug(transaction) - transaction.commit(label=f'Removing {template.identifier}...', - remove_empty_directories=remove_empty_directories) - del self.templates[real_template.name] - self.save() - - def list_template_files(self, include_system: bool = True, include_user: bool = True) -> List[str]: - files = [] - for t in self.templates.values(): - if include_system: - files.extend(t.system_files) - if include_user: - files.extend(t.user_files) - return files - - def resolve_template(self, query: Union[str, BaseTemplate]) -> List[Template]: - if isinstance(query, str): - query = BaseTemplate.create_query(query) - assert isinstance(query, BaseTemplate) - return [local_template for local_template in self.templates.values() if local_template.satisfies(query)] - - def __str__(self): - return f'Project: {self.location} ({self.name}) for {self.target} with ' \ - f'{", ".join([str(t) for t in self.templates.values()])}' - - @property - def kernel(self): - if 'kernel' in self.templates: - return self.templates['kernel'].version - elif hasattr(self.__dict__, 'kernel'): - return self.__dict__['kernel'] - return '' - - @property - def output(self): - if 'kernel' in self.templates: - return self.templates['kernel'].metadata['output'] - elif hasattr(self.__dict__, 'output'): - return self.__dict__['output'] - return 'bin/output.bin' - - def make(self, build_args: List[str]): - import subprocess - env = os.environ.copy() - # Add PROS toolchain to the beginning of PATH to ensure PROS binaries are preferred - if os.environ.get('PROS_TOOLCHAIN'): - env['PATH'] = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin') + os.pathsep + env['PATH'] - - # call make.exe if on Windows - if os.name == 'nt' and os.environ.get('PROS_TOOLCHAIN'): - make_cmd = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin', 'make.exe') - else: - make_cmd = 'make' - stdout_pipe = EchoPipe() - stderr_pipe = EchoPipe(err=True) - process=None - try: - process = subprocess.Popen(executable=make_cmd, args=[make_cmd, *build_args], cwd=self.directory, env=env, - stdout=stdout_pipe, stderr=stderr_pipe) - except Exception as e: - if not os.environ.get('PROS_TOOLCHAIN'): - ui.logger(__name__).warn("PROS toolchain not found! Please ensure the toolchain is installed correctly and your environment variables are set properly.\n") - ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}' WITH EXCEPTION: {str(e)}\n",extra={'sentry':False}) - stdout_pipe.close() - stderr_pipe.close() - sys.exit() - stdout_pipe.close() - stderr_pipe.close() - process.wait() - return process.returncode - - def make_scan_build(self, build_args: Tuple[str], cdb_file: Optional[Union[str, io.IOBase]] = None, - suppress_output: bool = False, sandbox: bool = False): - from libscanbuild.compilation import Compilation, CompilationDatabase - from libscanbuild.arguments import create_intercept_parser - import itertools - - import subprocess - import argparse - - if sandbox: - import tempfile - td = tempfile.TemporaryDirectory() - td_path = td.name.replace("\\", "/") - build_args = [*build_args, f'BINDIR={td_path}'] - - def libscanbuild_capture(args: argparse.Namespace) -> Tuple[int, Iterable[Compilation]]: - """ - Implementation of compilation database generation. - - :param args: the parsed and validated command line arguments - :return: the exit status of build process. - """ - from libscanbuild.intercept import setup_environment, run_build, exec_trace_files, parse_exec_trace, \ - compilations - from libear import temporary_directory - - with temporary_directory(prefix='intercept-') as tmp_dir: - # run the build command - environment = setup_environment(args, tmp_dir) - if os.environ.get('PROS_TOOLCHAIN'): - environment['PATH'] = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin') + os.pathsep + \ - environment['PATH'] - - if sys.platform == 'darwin': - environment['PATH'] = os.path.dirname(os.path.abspath(sys.executable)) + os.pathsep + \ - environment['PATH'] - - if not suppress_output: - pipe = EchoPipe() - else: - pipe = subprocess.DEVNULL - logger(__name__).debug(self.directory) - exit_code=None - try: - exit_code = run_build(args.build, env=environment, stdout=pipe, stderr=pipe, cwd=self.directory) - except Exception as e: - if not os.environ.get('PROS_TOOLCHAIN'): - ui.logger(__name__).warn("PROS toolchain not found! Please ensure the toolchain is installed correctly and your environment variables are set properly.\n") - ui.logger(__name__).error(f"ERROR WHILE CALLING '{make_cmd}' WITH EXCEPTION: {str(e)}\n",extra={'sentry':False}) - if not suppress_output: - pipe.close() - sys.exit() - if not suppress_output: - pipe.close() - # read the intercepted exec calls - calls = (parse_exec_trace(file) for file in exec_trace_files(tmp_dir)) - current = compilations(calls, args.cc, args.cxx) - - return exit_code, iter(set(current)) - - # call make.exe if on Windows - if os.name == 'nt' and os.environ.get('PROS_TOOLCHAIN'): - make_cmd = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin', 'make.exe') - else: - make_cmd = 'make' - args = create_intercept_parser().parse_args( - ['--override-compiler', '--use-cc', 'arm-none-eabi-gcc', '--use-c++', 'arm-none-eabi-g++', make_cmd, - *build_args, - 'CC=intercept-cc', 'CXX=intercept-c++']) - exit_code, entries = libscanbuild_capture(args) - - if sandbox and td: - td.cleanup() - - any_entries, entries = itertools.tee(entries, 2) - if not any(any_entries): - return exit_code - if not suppress_output: - ui.echo('Capturing metadata for PROS Editor...') - env = os.environ.copy() - # Add PROS toolchain to the beginning of PATH to ensure PROS binaries are preferred - if os.environ.get('PROS_TOOLCHAIN'): - env['PATH'] = os.path.join(os.environ.get('PROS_TOOLCHAIN'), 'bin') + os.pathsep + env['PATH'] - cc_sysroot = subprocess.run([make_cmd, 'cc-sysroot'], env=env, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, cwd=self.directory) - lines = str(cc_sysroot.stderr.decode()).splitlines() + str(cc_sysroot.stdout.decode()).splitlines() - lines = [l.strip() for l in lines] - cc_sysroot_includes = [] - copy = False - for line in lines: - if line == '#include <...> search starts here:': - copy = True - continue - if line == 'End of search list.': - copy = False - continue - if copy: - cc_sysroot_includes.append(f'-isystem{line}') - cxx_sysroot = subprocess.run([make_cmd, 'cxx-sysroot'], env=env, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, cwd=self.directory) - lines = str(cxx_sysroot.stderr.decode()).splitlines() + str(cxx_sysroot.stdout.decode()).splitlines() - lines = [l.strip() for l in lines] - cxx_sysroot_includes = [] - copy = False - for line in lines: - if line == '#include <...> search starts here:': - copy = True - continue - if line == 'End of search list.': - copy = False - continue - if copy: - cxx_sysroot_includes.append(f'-isystem{line}') - new_entries, entries = itertools.tee(entries, 2) - new_sources = set([e.source for e in entries]) - if not cdb_file: - cdb_file = os.path.join(self.directory, 'compile_commands.json') - if isinstance(cdb_file, str) and os.path.isfile(cdb_file): - old_entries = itertools.filterfalse(lambda entry: entry.source in new_sources, - CompilationDatabase.load(cdb_file)) - else: - old_entries = [] - - extra_flags = ['-target', 'armv7ar-none-none-eabi'] - logger(__name__).debug('cc_sysroot_includes') - logger(__name__).debug(cc_sysroot_includes) - logger(__name__).debug('cxx_sysroot_includes') - logger(__name__).debug(cxx_sysroot_includes) - - if sys.platform == 'win32': - extra_flags.extend(["-fno-ms-extensions", "-fno-ms-compatibility", "-fno-delayed-template-parsing"]) - - def new_entry_map(entry): - if entry.compiler == 'c': - entry.flags = extra_flags + cc_sysroot_includes + entry.flags - elif entry.compiler == 'c++': - entry.flags = extra_flags + cxx_sysroot_includes + entry.flags - return entry - - new_entries = map(new_entry_map, new_entries) - - def entry_map(entry: Compilation): - json_entry = entry.as_db_entry() - json_entry['arguments'][0] = 'clang' if entry.compiler == 'c' else 'clang++' - return json_entry - - entries = itertools.chain(old_entries, new_entries) - json_entries = list(map(entry_map, entries)) - if isinstance(cdb_file, str): - cdb_file = open(cdb_file, 'w') - import json - json.dump(json_entries, cdb_file, sort_keys=True, indent=4) - - return exit_code - - def compile(self, build_args: List[str], scan_build: Optional[bool] = None): - if scan_build is None: - from pros.config.cli_config import cli_config - scan_build = cli_config().use_build_compile_commands - return self.make_scan_build(build_args) if scan_build else self.make(build_args) - - @staticmethod - def find_project(path: str, recurse_times: int = 10): - path = os.path.abspath(path or '.') - if os.path.isfile(path): - path = os.path.dirname(path) - if os.path.isdir(path): - for n in range(recurse_times): - if path is not None and os.path.isdir(path): - files = [f for f in os.listdir(path) - if os.path.isfile(os.path.join(path, f)) and f.lower() == 'project.pros'] - if len(files) == 1: # found a project.pros file! - logger(__name__).info(f'Found Project Path: {os.path.join(path, files[0])}') - return os.path.join(path, files[0]) - path = os.path.dirname(path) - else: - return None - return None - - -__all__ = ['Project', 'ProjectReport'] diff --git a/pros/conductor/project/template_resolution.py b/pros/conductor/project/template_resolution.py deleted file mode 100644 index f9b9aeb0..00000000 --- a/pros/conductor/project/template_resolution.py +++ /dev/null @@ -1,18 +0,0 @@ -from enum import Flag, auto - - -class TemplateAction(Flag): - NotApplicable = auto() - Installable = auto() - Upgradable = auto() - AlreadyInstalled = auto() - Downgradable = auto() - - UnforcedApplicable = Installable | Upgradable | Downgradable - ForcedApplicable = UnforcedApplicable | AlreadyInstalled - - -class InvalidTemplateException(Exception): - def __init__(self, *args, reason: TemplateAction = None): - self.reason = reason - super(InvalidTemplateException, self).__init__(*args) diff --git a/pros/conductor/templates/__init__.py b/pros/conductor/templates/__init__.py deleted file mode 100644 index 2d402886..00000000 --- a/pros/conductor/templates/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .base_template import BaseTemplate -from .external_template import ExternalTemplate -from .local_template import LocalTemplate -from .template import Template diff --git a/pros/conductor/templates/base_template.py b/pros/conductor/templates/base_template.py deleted file mode 100644 index 95a19064..00000000 --- a/pros/conductor/templates/base_template.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import * - -from semantic_version import Spec, Version - -from pros.common import ui - - -class BaseTemplate(object): - def __init__(self, **kwargs): - self.name: str = None - self.version: str = None - self.supported_kernels: str = None - self.target: str = None - self.metadata: Dict[str, Any] = {} - if 'orig' in kwargs: - self.__dict__.update({k: v for k, v in kwargs.pop('orig').__dict__.items() if k in self.__dict__}) - self.__dict__.update({k: v for k, v in kwargs.items() if k in self.__dict__}) - self.metadata.update({k: v for k, v in kwargs.items() if k not in self.__dict__}) - if 'depot' in self.metadata and 'origin' not in self.metadata: - self.metadata['origin'] = self.metadata.pop('depot') - if 'd' in self.metadata and 'depot' not in self.metadata: - self.metadata['depot'] = self.metadata.pop('d') - if 'l' in self.metadata and 'location' not in self.metadata: - self.metadata['location'] = self.metadata.pop('l') - if self.name == 'pros': - self.name = 'kernel' - - def satisfies(self, query: 'BaseTemplate', kernel_version: Union[str, Version] = None) -> bool: - if query.name and self.name != query.name: - return False - if query.target and self.target != query.target: - return False - if query.version and Version(self.version) not in Spec(query.version): - return False - if kernel_version and isinstance(kernel_version, str): - kernel_version = Version(kernel_version) - if self.supported_kernels and kernel_version and kernel_version not in Spec(self.supported_kernels): - return False - keys_intersection = set(self.metadata.keys()).intersection(query.metadata.keys()) - # Find the intersection of the keys in the template's metadata with the keys in the query metadata - # This is what allows us to throw all arguments into the query metadata (from the CLI, e.g. those intended - # for the depot or template application hints) - if any([self.metadata[k] != query.metadata[k] for k in keys_intersection]): - return False - return True - - def __str__(self): - fields = [self.metadata.get("origin", None), self.target, self.__class__.__name__] - additional = ", ".join(map(str, filter(bool, fields))) - return f'{self.identifier} ({additional})' - - def __gt__(self, other): - if isinstance(other, BaseTemplate): - # TODO: metadata comparison - return self.name == other.name and Version(self.version) > Version(other.version) - else: - return False - - def __eq__(self, other): - if isinstance(other, BaseTemplate): - return self.identifier == other.identifier - else: - return super().__eq__(other) - - def __hash__(self): - return self.identifier.__hash__() - - def as_query(self, version='>0', metadata=False, **kwargs): - if isinstance(metadata, bool) and not metadata: - metadata = dict() - return BaseTemplate(orig=self, version=version, metadata=metadata, **kwargs) - - @property - def identifier(self): - return f'{self.name}@{self.version}' - - @property - def origin(self): - return self.metadata.get('origin', 'Unknown') - - @classmethod - def create_query(cls, name: str = None, **kwargs) -> 'BaseTemplate': - if not isinstance(name, str): - return cls(**kwargs) - if name.count('@') > 1: - raise ValueError(f'Malformed identifier: {name}') - if '@' in name: - name, kwargs['version'] = name.split('@') - if kwargs.get('version', 'latest') == 'latest': - kwargs['version'] = '>=0' - if name == 'kernal': - ui.echo("Assuming 'kernal' is the British spelling of kernel.") - name = 'kernel' - return cls(name=name, **kwargs) diff --git a/pros/conductor/templates/external_template.py b/pros/conductor/templates/external_template.py deleted file mode 100644 index ce08662e..00000000 --- a/pros/conductor/templates/external_template.py +++ /dev/null @@ -1,27 +0,0 @@ -import os.path -import tempfile -import zipfile - -from pros.config import Config - -from .template import Template - - -class ExternalTemplate(Config, Template): - def __init__(self, file: str, **kwargs): - if os.path.isdir(file): - file = os.path.join(file, 'template.pros') - elif zipfile.is_zipfile(file): - self.tf = tempfile.NamedTemporaryFile(delete=False) - with zipfile.ZipFile(file) as zf: - with zf.open('template.pros') as zt: - self.tf.write(zt.read()) - self.tf.seek(0, 0) - file = self.tf.name - error_on_decode = kwargs.pop('error_on_decode', False) - Template.__init__(self, **kwargs) - Config.__init__(self, file, error_on_decode=error_on_decode) - - def __del__(self): - if hasattr(self, 'tr'): - del self.tf diff --git a/pros/conductor/templates/local_template.py b/pros/conductor/templates/local_template.py deleted file mode 100644 index 53d66e73..00000000 --- a/pros/conductor/templates/local_template.py +++ /dev/null @@ -1,24 +0,0 @@ -import os - -from .template import Template - - -def _fix_path(*paths: str) -> str: - return os.path.normpath(os.path.join(*paths).replace('\\', '/')) - - -class LocalTemplate(Template): - def __init__(self, **kwargs): - self.location: str = None - super().__init__(**kwargs) - - @property - def real_user_files(self): - return filter(lambda f: os.path.exists(_fix_path(self.location, f)), self.user_files) - - @property - def real_system_files(self): - return filter(lambda f: os.path.exists(_fix_path(self.location, f)), self.system_files) - - def __hash__(self): - return self.identifier.__hash__() diff --git a/pros/conductor/templates/template.py b/pros/conductor/templates/template.py deleted file mode 100644 index 12aaa1f3..00000000 --- a/pros/conductor/templates/template.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import * - -from semantic_version import Version - -from .base_template import BaseTemplate - - -class Template(BaseTemplate): - def __init__(self, **kwargs): - self.system_files: List[str] = [] - self.user_files: List[str] = [] - super().__init__(**kwargs) - if self.version: - self.version = str(Version.coerce(self.version)) - - @property - def all_files(self) -> Set[str]: - return {*self.system_files, *self.user_files} diff --git a/pros/conductor/transaction.py b/pros/conductor/transaction.py deleted file mode 100644 index 0fcb05d7..00000000 --- a/pros/conductor/transaction.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -import shutil -from typing import * - -import pros.common.ui as ui -from pros.common import logger - - -class Transaction(object): - def __init__(self, location: str, current_state: Set[str]): - self._add_files: Set[str] = set() - self._rm_files: Set[str] = set() - self._add_srcs: Dict[str, str] = {} - self.effective_state = current_state - self.location: str = location - - def extend_add(self, paths: Iterable[str], src: str): - for path in paths: - self.add(path, src) - - def add(self, path: str, src: str): - path = os.path.normpath(path.replace('\\', '/')) - self._add_files.add(path) - self.effective_state.add(path) - self._add_srcs[path] = src - if path in self._rm_files: - self._rm_files.remove(path) - - def extend_rm(self, paths: Iterable[str]): - for path in paths: - self.rm(path) - - def rm(self, path: str): - path = os.path.normpath(path.replace('\\', '/')) - self._rm_files.add(path) - if path in self.effective_state: - self.effective_state.remove(path) - if path in self._add_files: - self._add_files.remove(path) - self._add_srcs.pop(path) - - def commit(self, label: str = 'Committing transaction', remove_empty_directories: bool = True): - with ui.progressbar(length=len(self._rm_files) + len(self._add_files), label=label) as pb: - for file in sorted(self._rm_files, key=lambda p: p.count('/') + p.count('\\'), reverse=True): - file_path = os.path.join(self.location, file) - if os.path.isfile(file_path): - logger(__name__).info(f'Removing {file}') - os.remove(os.path.join(self.location, file)) - else: - logger(__name__).info(f'Not removing nonexistent {file}') - pardir = os.path.abspath(os.path.join(file_path, os.pardir)) - while remove_empty_directories and len(os.listdir(pardir)) == 0: - logger(__name__).info(f'Removing {os.path.relpath(pardir, self.location)}') - os.rmdir(pardir) - pardir = os.path.abspath(os.path.join(pardir, os.pardir)) - if pardir == self.location: - # Don't try and recursively delete folders outside the scope of the - # transaction directory - break - pb.update(1) - for file in self._add_files: - source = os.path.join(self._add_srcs[file], file) - destination = os.path.join(self.location, file) - if os.path.isfile(source): - if not os.path.isdir(os.path.dirname(destination)): - logger(__name__).debug(f'Creating directories: f{destination}') - os.makedirs(os.path.dirname(destination), exist_ok=True) - logger(__name__).info(f'Adding {file}') - shutil.copy(os.path.join(self._add_srcs[file], file), os.path.join(self.location, file)) - else: - logger(__name__).info(f"Not copying {file} because {source} doesn't exist.") - pb.update(1) - - def __str__(self): - return f'Transaction Object: ADD: {self._add_files}\tRM: {self._rm_files}\tLocation: {self.location}' diff --git a/pros/config/__init__.py b/pros/config/__init__.py deleted file mode 100644 index 8c5b70ca..00000000 --- a/pros/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pros.config.config import Config, ConfigNotFoundException diff --git a/pros/config/cli_config.py b/pros/config/cli_config.py deleted file mode 100644 index 8c962047..00000000 --- a/pros/config/cli_config.py +++ /dev/null @@ -1,69 +0,0 @@ -import json.decoder -import os.path -from datetime import datetime, timedelta -from typing import * - -import click - -import pros.common -# import pros.conductor.providers.github_releases as githubreleases -from pros.config.config import Config - -if TYPE_CHECKING: - from pros.upgrade.manifests.upgrade_manifest_v1 import UpgradeManifestV1 # noqa: F401 - - -class CliConfig(Config): - def __init__(self, file=None): - if not file: - file = os.path.join(click.get_app_dir('PROS'), 'cli.pros') - self.update_frequency: timedelta = timedelta(hours=1) - self.override_use_build_compile_commands: Optional[bool] = None - self.offer_sentry: Optional[bool] = None - self.ga: Optional[dict] = None - super(CliConfig, self).__init__(file) - - def needs_online_fetch(self, last_fetch: datetime) -> bool: - return datetime.now() - last_fetch > self.update_frequency - - @property - def use_build_compile_commands(self): - if self.override_use_build_compile_commands is not None: - return self.override_use_build_compile_commands - paths = [os.path.join('~', '.pros-atom'), os.path.join('~', '.pros-editor')] - return any([os.path.exists(os.path.expanduser(p)) for p in paths]) - - def get_upgrade_manifest(self, force: bool = False) -> Optional['UpgradeManifestV1']: - from pros.upgrade.manifests.upgrade_manifest_v1 import UpgradeManifestV1 # noqa: F811 - - if not force and not self.needs_online_fetch(self.cached_upgrade[0]): - return self.cached_upgrade[1] - pros.common.logger(__name__).info('Fetching upgrade manifest...') - import requests - import jsonpickle - r = requests.get('https://purduesigbots.github.io/pros-mainline/cli-updates.json') - pros.common.logger(__name__).debug(r) - if r.status_code == 200: - try: - self.cached_upgrade = (datetime.now(), jsonpickle.decode(r.text)) - except json.decoder.JSONDecodeError: - return None - assert isinstance(self.cached_upgrade[1], UpgradeManifestV1) - pros.common.logger(__name__).debug(self.cached_upgrade[1]) - self.save() - return self.cached_upgrade[1] - else: - pros.common.logger(__name__).warning(f'Failed to fetch CLI updates because status code: {r.status_code}') - pros.common.logger(__name__).debug(r) - return None - - -def cli_config() -> CliConfig: - ctx = click.get_current_context(silent=True) - if not ctx or not isinstance(ctx, click.Context): - return CliConfig() - ctx.ensure_object(dict) - assert isinstance(ctx.obj, dict) - if not hasattr(ctx.obj, 'cli_config') or not isinstance(ctx.obj['cli_config'], CliConfig): - ctx.obj['cli_config'] = CliConfig() - return ctx.obj['cli_config'] diff --git a/pros/config/config.py b/pros/config/config.py deleted file mode 100644 index c7250620..00000000 --- a/pros/config/config.py +++ /dev/null @@ -1,110 +0,0 @@ -import json.decoder - -import jsonpickle -from pros.common.utils import * - - -class ConfigNotFoundException(Exception): - def __init__(self, message, *args, **kwargs): - super(ConfigNotFoundException, self).__init__(args, kwargs) - self.message = message - - -class Config(object): - """ - A configuration object that's capable of being saved as a JSON object - """ - - def __init__(self, file, error_on_decode=False): - logger(__name__).debug('Opening {} ({})'.format(file, self.__class__.__name__)) - self.save_file = file - # __ignored property has any fields which shouldn't be included the pickled config file - self.__ignored = self.__dict__.get('_Config__ignored', []) - self.__ignored.append('save_file') - self.__ignored.append('_Config__ignored') - if file: - # If the file already exists, update this new config with the values in the file - if os.path.isfile(file): - with open(file, 'r', encoding ='utf-8') as f: - try: - result = jsonpickle.decode(f.read()) - if isinstance(result, dict): - if 'py/state' in result: - class_name = '{}.{}'.format(self.__class__.__module__, self.__class__.__qualname__) - logger(__name__).debug( - 'Coercing {} to {}'.format(result['py/object'], class_name)) - old_object = result['py/object'] - try: - result['py/object'] = class_name - result = jsonpickle.unpickler.Unpickler().restore(result) - except (json.decoder.JSONDecodeError, AttributeError) as e: - logger(__name__).debug(e) - logger(__name__).warning(f'Couldn\'t coerce {file} ({old_object}) to ' - f'{class_name}. Using rudimentary coercion') - self.__dict__.update(result['py/state']) - else: - self.__dict__.update(result) - elif isinstance(result, object): - self.__dict__.update(result.__dict__) - except (json.decoder.JSONDecodeError, AttributeError, UnicodeDecodeError) as e: - if error_on_decode: - logger(__name__).error(f'Error parsing {file}') - logger(__name__).exception(e) - raise e - else: - logger(__name__).debug(e) - pass - # obvious - elif os.path.isdir(file): - raise ValueError('{} must be a file, not a directory'.format(file)) - # The file didn't exist when we created, so we'll save the default values - else: - try: - self.save() - except Exception as e: - if error_on_decode: - logger(__name__).exception(e) - raise e - else: - logger(__name__).debug('Failed to save {} ({})'.format(file, e)) - - from pros.common.sentry import add_context - add_context(self) - - def __getstate__(self): - state = self.__dict__.copy() - if '_Config__ignored' in self.__dict__: - for key in [k for k in self.__ignored if k in state]: - del state[key] - return state - - def __setstate__(self, state): - self.__dict__.update(state) - - def __str__(self): - jsonpickle.set_encoder_options('json', sort_keys=True) - return jsonpickle.encode(self) - - def delete(self): - if os.path.isfile(self.save_file): - os.remove(self.save_file) - - def save(self, file: str = None) -> None: - if file is None: - file = self.save_file - jsonpickle.set_encoder_options('json', sort_keys=True, indent=4) - if os.path.dirname(file): - os.makedirs(os.path.dirname(file), exist_ok=True) - with open(file, 'w') as f: - f.write(jsonpickle.encode(self)) - logger(__name__).debug('Saved {}'.format(file)) - - def migrate(self, migration): - for (old, new) in migration.iteritems(): - if self.__dict__.get(old) is not None: - self.__dict__[new] = self.__dict__[old] - del self.__dict__[old] - - @property - def directory(self) -> str: - return os.path.dirname(os.path.abspath(self.save_file)) diff --git a/pros/ga/__init__.py b/pros/ga/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pros/ga/analytics.py b/pros/ga/analytics.py deleted file mode 100644 index 6f786105..00000000 --- a/pros/ga/analytics.py +++ /dev/null @@ -1,95 +0,0 @@ -import json -from os import path -import uuid -import requests -from requests_futures.sessions import FuturesSession -import random -from concurrent.futures import as_completed - -url = 'https://www.google-analytics.com/collect' -agent = 'pros-cli' - -""" -PROS ANALYTICS CLASS -""" - -class Analytics(): - def __init__(self): - from pros.config.cli_config import cli_config as get_cli_config - self.cli_config = get_cli_config() - #If GA hasn't been setup yet (first time install/update) - if not self.cli_config.ga: - #Default values for GA - self.cli_config.ga = { - "enabled": "True", - "ga_id": "UA-84548828-8", - "u_id": str(uuid.uuid4()) - } - self.cli_config.save() - self.sent = False - #Variables that the class will use - self.gaID = self.cli_config.ga['ga_id'] - self.useAnalytics = self.cli_config.ga['enabled'] - self.uID = self.cli_config.ga['u_id'] - self.pendingRequests = [] - - def send(self,action): - if not self.useAnalytics or self.sent: - return - self.sent=True # Prevent Send from being called multiple times - try: - #Payload to be sent to GA, idk what some of them are but it works - payload = { - 'v': 1, - 'tid': self.gaID, - 'aip': 1, - 'z': random.random(), - 'cid': self.uID, - 't': 'event', - 'ec': 'action', - 'ea': action, - 'el': 'CLI', - 'ev': '1', - 'ni': 0 - } - - session = FuturesSession() - - #Send payload to GA servers - future = session.post(url=url, - data=payload, - headers={'User-Agent': agent}, - timeout=5.0) - self.pendingRequests.append(future) - - except Exception as e: - from pros.cli.common import logger - logger(__name__).warning("Unable to send analytics. Do you have a stable internet connection?", extra={'sentry': False}) - - def set_use(self, value: bool): - #Sets if GA is being used or not - self.useAnalytics = value - self.cli_config.ga['enabled'] = self.useAnalytics - self.cli_config.save() - - def process_requests(self): - responses = [] - for future in as_completed(self.pendingRequests): - try: - response = future.result() - - if not response.status_code==200: - print("Something went wrong while sending analytics!") - print(response) - - responses.append(response) - - except Exception: - print("Something went wrong while sending analytics!") - - - self.pendingRequests.clear() - return responses - - -analytics = Analytics() \ No newline at end of file diff --git a/pros/jinx/__init__.py b/pros/jinx/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pros/jinx/server.py b/pros/jinx/server.py deleted file mode 100644 index 31f848cf..00000000 --- a/pros/jinx/server.py +++ /dev/null @@ -1,6 +0,0 @@ -from pros.serial.devices import StreamDevice - - -class JinxServer(object): - def __init__(self, device: StreamDevice): - self.device = device diff --git a/pros/serial/__init__.py b/pros/serial/__init__.py deleted file mode 100644 index 0177d021..00000000 --- a/pros/serial/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Union - - -def bytes_to_str(arr): - if isinstance(arr, str): - arr = bytes(arr) - if hasattr(arr, '__iter__'): - return ''.join('{:02X} '.format(x) for x in arr).strip() - else: # actually just a single byte - return '0x{:02X}'.format(arr) - - -def decode_bytes_to_str(data: Union[bytes, bytearray], encoding: str = 'utf-8', errors: str = 'strict') -> str: - return data.split(b'\0', 1)[0].decode(encoding=encoding, errors=errors) diff --git a/pros/serial/devices/__init__.py b/pros/serial/devices/__init__.py deleted file mode 100644 index ac6cd8c0..00000000 --- a/pros/serial/devices/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .generic_device import GenericDevice -from .stream_device import StreamDevice, RawStreamDevice diff --git a/pros/serial/devices/generic_device.py b/pros/serial/devices/generic_device.py deleted file mode 100644 index 0e139fc8..00000000 --- a/pros/serial/devices/generic_device.py +++ /dev/null @@ -1,13 +0,0 @@ -from ..ports import BasePort - - -class GenericDevice(object): - def __init__(self, port: BasePort): - self.port = port - - def destroy(self): - self.port.destroy() - - @property - def name(self): - return self.port.name diff --git a/pros/serial/devices/stream_device.py b/pros/serial/devices/stream_device.py deleted file mode 100644 index 2649af97..00000000 --- a/pros/serial/devices/stream_device.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import * - -from .generic_device import GenericDevice - - -class StreamDevice(GenericDevice): - def subscribe(self, topic: bytes): - raise NotImplementedError - - def unsubscribe(self, topic: bytes): - raise NotImplementedError - - @property - def promiscuous(self): - raise NotImplementedError - - @promiscuous.setter - def promiscuous(self, value: bool): - raise NotImplementedError - - def read(self) -> Tuple[bytes, bytes]: - raise NotImplementedError - - def write(self, data: Union[bytes, str]): - raise NotImplementedError - - -class RawStreamDevice(StreamDevice): - - def subscribe(self, topic: bytes): - pass - - def unsubscribe(self, topic: bytes): - pass - - @property - def promiscuous(self): - return False - - @promiscuous.setter - def promiscuous(self, value: bool): - pass - - def read(self) -> Tuple[bytes, bytes]: - return b'', self.port.read_all() - - def write(self, data: Union[bytes, str]): - self.port.write(data) diff --git a/pros/serial/devices/system_device.py b/pros/serial/devices/system_device.py deleted file mode 100644 index 6511c4cd..00000000 --- a/pros/serial/devices/system_device.py +++ /dev/null @@ -1,11 +0,0 @@ -import typing - -from pros.conductor import Project - - -class SystemDevice(object): - def upload_project(self, project: Project, **kwargs): - raise NotImplementedError - - def write_program(self, file: typing.BinaryIO, quirk: int = 0, **kwargs): - raise NotImplementedError diff --git a/pros/serial/devices/vex/__init__.py b/pros/serial/devices/vex/__init__.py deleted file mode 100644 index 34665777..00000000 --- a/pros/serial/devices/vex/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .comm_error import VEXCommError -from .cortex_device import CortexDevice, find_cortex_ports -from .v5_device import V5Device, find_v5_ports -from .v5_user_device import V5UserDevice -from .vex_device import VEXDevice diff --git a/pros/serial/devices/vex/comm_error.py b/pros/serial/devices/vex/comm_error.py deleted file mode 100644 index e2eaf9b0..00000000 --- a/pros/serial/devices/vex/comm_error.py +++ /dev/null @@ -1,7 +0,0 @@ -class VEXCommError(Exception): - def __init__(self, message, msg): - self.message = message - self.msg = msg - - def __str__(self): - return "{}\n{}".format(self.message, self.msg) diff --git a/pros/serial/devices/vex/cortex_device.py b/pros/serial/devices/vex/cortex_device.py deleted file mode 100644 index 02dbfe0f..00000000 --- a/pros/serial/devices/vex/cortex_device.py +++ /dev/null @@ -1,154 +0,0 @@ -import itertools -import time -import typing -from enum import IntFlag -from pathlib import Path -from typing import * - -from pros.common import ui -from pros.common.utils import retries, logger -from pros.conductor import Project -from pros.serial import bytes_to_str -from pros.serial.devices.vex import VEXCommError -from pros.serial.devices.vex.stm32_device import STM32Device -from pros.serial.ports import list_all_comports - -from .vex_device import VEXDevice -from ..system_device import SystemDevice - - -def find_cortex_ports(): - return [p for p in list_all_comports() if p.vid is not None and p.vid in [0x4D8, 0x67B]] - - -class CortexDevice(VEXDevice, SystemDevice): - class SystemStatus(object): - def __init__(self, data: Tuple[bytes, ...]): - self.joystick_firmware = data[0:2] - self.robot_firmware = data[2:4] - self.joystick_battery = float(data[4]) * .059 - self.robot_battery = float(data[5]) * .059 - self.backup_battery = float(data[6]) * .059 - self.flags = CortexDevice.SystemStatusFlags(data[7]) - - def __str__(self): - return f' Tether: {str(self.flags)}\n' \ - f' Cortex: F/W {self.robot_firmware[0]}.{self.robot_firmware[1]} w/ {self.robot_battery:1.2f} V ' \ - f'(Backup: {self.backup_battery:1.2f} V)\n' \ - f'Joystick: F/W {self.joystick_firmware[0]}.{self.robot_firmware[1]} w/ ' \ - f'{self.joystick_battery:1.2f} V' - - class SystemStatusFlags(IntFlag): - DL_MODE = (1 << 0) - TETH_VN2 = (1 << 2) - FCS_CONNECT = (1 << 3) - TETH_USB = (1 << 4) - DIRECT_USB = (1 << 5) - FCS_AUTON = (1 << 6) - FCS_DISABLE = (1 << 7) - - TETH_BITS = DL_MODE | TETH_VN2 | TETH_USB - - def __str__(self): - def andeq(a, b): - return (a & b) == b - - if not self.value & self.TETH_BITS: - s = 'Serial w/VEXnet 1.0 Keys' - elif andeq(self.value, 0x01): - s = 'Serial w/VEXnet 1.0 Keys (turbo)' - elif andeq(self.value, 0x04): - s = 'Serial w/VEXnet 2.0 Keys' - elif andeq(self.value, 0x05): - s = 'Serial w/VEXnet 2.0 Keys (download mode)' - elif andeq(self.value, 0x10): - s = 'Serial w/ a USB Cable' - elif andeq(self.value, 0x20): - s = 'Directly w/ a USB Cable' - else: - s = 'Unknown' - - if andeq(self.value, self.FCS_CONNECT): - s += ' - FCS Connected' - return s - - def get_connected_device(self) -> SystemDevice: - logger(__name__).info('Interrogating Cortex...') - stm32 = STM32Device(self.port, do_negoitate=False) - try: - stm32.get(n_retries=1) - return stm32 - except VEXCommError: - return self - - def upload_project(self, project: Project, **kwargs): - assert project.target == 'cortex' - output_path = project.path.joinpath(project.output) - if not output_path.exists(): - raise ui.dont_send(Exception('No output files were found! Have you built your project?')) - with output_path.open(mode='rb') as pf: - return self.write_program(pf, **kwargs) - - def write_program(self, file: typing.BinaryIO, **kwargs): - action_string = '' - if hasattr(file, 'name'): - action_string += f' {Path(file.name).name}' - action_string += f' to Cortex on {self.port}' - ui.echo(f'Uploading {action_string}') - - logger(__name__).info('Writing program to Cortex') - status = self.query_system() - logger(__name__).info(status) - if not status.flags | self.SystemStatusFlags.TETH_USB and not status.flags | self.SystemStatusFlags.DL_MODE: - self.send_to_download_channel() - - bootloader = self.expose_bootloader() - rv = bootloader.write_program(file, **kwargs) - - ui.finalize('upload', f'Finished uploading {action_string}') - return rv - - @retries - def query_system(self) -> SystemStatus: - logger(__name__).info('Querying system information') - rx = self._txrx_simple_struct(0x21, "<8B2x") - status = CortexDevice.SystemStatus(rx) - ui.finalize('cortex-status', status) - return status - - @retries - def send_to_download_channel(self): - logger(__name__).info('Sending to download channel') - self._txrx_ack_packet(0x35, timeout=1.0) - - @retries - def expose_bootloader(self): - logger(__name__).info('Exposing bootloader') - for _ in itertools.repeat(None, 5): - self._tx_packet(0x25) - time.sleep(0.1) - self.port.read_all() - time.sleep(0.3) - return STM32Device(self.port, must_initialize=True) - - def _rx_ack(self, timeout: float = 0.01): - # Optimized to read as quickly as possible w/o delay - start_time = time.time() - while time.time() - start_time < timeout: - if self.port.read(1)[0] == self.ACK_BYTE: - return - raise IOError("Device never ACK'd") - - def _txrx_ack_packet(self, command: int, timeout=0.1): - """ - Goes through a send/receive cycle with a VEX device. - Transmits the command with the optional additional payload, then reads and parses the outer layer - of the response - :param command: Command to send the device - :param retries: Number of retries to attempt to parse the output before giving up and raising an error - :return: Returns a dictionary containing the received command field and the payload. Correctly computes - the payload length even if the extended command (0x56) is used (only applies to the V5). - """ - tx = self._tx_packet(command) - self._rx_ack(timeout=timeout) - logger(__name__).debug('TX: {}'.format(bytes_to_str(tx))) diff --git a/pros/serial/devices/vex/crc.py b/pros/serial/devices/vex/crc.py deleted file mode 100644 index f53bee5d..00000000 --- a/pros/serial/devices/vex/crc.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import * - - -class CRC: - def __init__(self, size: int, polynomial: int): - self._size = size - self._polynomial = polynomial - self._table = [] - - for i in range(256): - crc_accumulator = i << (self._size - 8) - for j in range(8): - if crc_accumulator & (1 << (self._size - 1)): - crc_accumulator = (crc_accumulator << 1) ^ self._polynomial - else: - crc_accumulator = (crc_accumulator << 1) - self._table.append(crc_accumulator) - - def compute(self, data: Iterable[int], accumulator: int = 0): - for d in data: - i = ((accumulator >> (self._size - 8)) ^ d) & 0xff - accumulator = ((accumulator << 8) ^ self._table[i]) & ((1 << self._size) - 1) - return accumulator diff --git a/pros/serial/devices/vex/message.py b/pros/serial/devices/vex/message.py deleted file mode 100644 index 8a45b0c4..00000000 --- a/pros/serial/devices/vex/message.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import * - -from pros.serial import bytes_to_str - - -class Message(bytes): - def __new__(cls, rx: bytes, tx: bytes, internal_rx: Union[bytes, int] = None, - bookmarks: Dict[str, bytes] = None): - if internal_rx is None: - internal_rx = rx - if isinstance(internal_rx, int): - internal_rx = bytes([internal_rx]) - return super().__new__(cls, internal_rx) - - def __init__(self, rx: bytes, tx: bytes, internal_rx: Union[bytes, int] = None, - bookmarks: Dict[str, bytes] = None): - if internal_rx is None: - internal_rx = rx - if isinstance(internal_rx, int): - internal_rx = bytes([internal_rx]) - self.rx = rx - self.tx = tx - self.internal_rx = internal_rx - self.bookmarks = {} if bookmarks is None else bookmarks - super().__init__() - - def __getitem__(self, item): - if isinstance(item, str) and item in self.bookmarks.keys(): - return self.bookmarks[item] - if isinstance(item, int): - return super().__getitem__(item) - return type(self)(self.rx, self.tx, internal_rx=self.internal_rx[item], bookmarks=self.bookmarks) - - def __setitem__(self, key, value): - self.bookmarks[key] = value - - def __str__(self): - return 'TX:{}\tRX:{}'.format(bytes_to_str(self.tx), bytes_to_str(self.rx)) diff --git a/pros/serial/devices/vex/stm32_device.py b/pros/serial/devices/vex/stm32_device.py deleted file mode 100644 index eecfdc47..00000000 --- a/pros/serial/devices/vex/stm32_device.py +++ /dev/null @@ -1,191 +0,0 @@ -import itertools -import operator -import struct -import time -import typing -from functools import reduce -from typing import * - -import pros.common.ui as ui -from pros.common import logger, retries -from pros.serial import bytes_to_str -from pros.serial.devices.vex import VEXCommError -from pros.serial.ports import BasePort - -from ..generic_device import GenericDevice -from ..system_device import SystemDevice - - -class STM32Device(GenericDevice, SystemDevice): - ACK_BYTE = 0x79 - NACK_BYTE = 0xFF - NUM_PAGES = 0xff - PAGE_SIZE = 0x2000 - - def __init__(self, port: BasePort, must_initialize: bool = False, do_negoitate: bool = True): - super().__init__(port) - self.commands = bytes([0x00, 0x01, 0x02, 0x11, 0x21, 0x31, 0x43, 0x63, 0x73, 0x82, 0x92]) - - if do_negoitate: - # self.port.write(b'\0' * 255) - if must_initialize: - self._txrx_command(0x7f, checksum=False) - try: - self.get(n_retries=0) - except: - logger(__name__).info('Sending bootloader initialization') - time.sleep(0.01) - self.port.rts = 0 - for _ in itertools.repeat(None, times=3): - time.sleep(0.01) - self._txrx_command(0x7f, checksum=False) - time.sleep(0.01) - self.get() - - def write_program(self, file: typing.BinaryIO, preserve_fs: bool = False, go_after: bool = True, **_): - file_len = file.seek(0, 2) - file.seek(0, 0) - if file_len > (self.NUM_PAGES * self.PAGE_SIZE): - raise VEXCommError( - f'File is too big to be uploaded (max file size: {self.NUM_PAGES * self.PAGE_SIZE} bytes)') - - if hasattr(file, 'name'): - display_name = file.name - else: - display_name = '(memory)' - - if not preserve_fs: - self.erase_all() - else: - self.erase_memory(list(range(0, int(file_len / self.PAGE_SIZE) + 1))) - - address = 0x08000000 - with ui.progressbar(length=file_len, label=f'Uploading {display_name}') as progress: - for i in range(0, file_len, 256): - write_size = 256 - if i + 256 > file_len: - write_size = file_len - i - self.write_memory(address, file.read(write_size)) - address += write_size - progress.update(write_size) - - if go_after: - self.go(0x08000000) - - def scan_prosfs(self): - pass - - @retries - def get(self): - logger(__name__).info('STM32: Get') - self._txrx_command(0x00) - n_bytes = self.port.read(1)[0] - assert n_bytes == 11 - data = self.port.read(n_bytes + 1) - logger(__name__).info(f'STM32 Bootloader version 0x{data[0]:x}') - self.commands = data[1:] - logger(__name__).debug(f'STM32 Bootloader commands are: {bytes_to_str(data[1:])}') - assert self.port.read(1)[0] == self.ACK_BYTE - - @retries - def get_read_protection_status(self): - logger(__name__).info('STM32: Get ID & Read Protection Status') - self._txrx_command(0x01) - data = self.port.read(3) - logger(__name__).debug(f'STM32 Bootloader Get Version & Read Protection Status is: {bytes_to_str(data)}') - assert self.port.read(1)[0] == self.ACK_BYTE - - @retries - def get_id(self): - logger(__name__).info('STM32: Get PID') - self._txrx_command(0x02) - n_bytes = self.port.read(1)[0] - pid = self.port.read(n_bytes + 1) - logger(__name__).debug(f'STM32 Bootloader PID is {pid}') - - @retries - def read_memory(self, address: int, n_bytes: int): - logger(__name__).info(f'STM32: Read {n_bytes} fromo 0x{address:x}') - assert 255 >= n_bytes > 0 - self._txrx_command(0x11) - self._txrx_command(struct.pack('>I', address)) - self._txrx_command(n_bytes) - return self.port.read(n_bytes) - - @retries - def go(self, start_address: int): - logger(__name__).info(f'STM32: Go 0x{start_address:x}') - self._txrx_command(0x21) - try: - self._txrx_command(struct.pack('>I', start_address), timeout=5.) - except VEXCommError: - logger(__name__).warning('STM32 Bootloader did not acknowledge GO command. ' - 'The program may take a moment to begin running ' - 'or the device should be rebooted.') - - @retries - def write_memory(self, start_address: int, data: bytes): - logger(__name__).info(f'STM32: Write {len(data)} to 0x{start_address:x}') - assert 0 < len(data) <= 256 - if len(data) % 4 != 0: - data = data + (b'\0' * (4 - (len(data) % 4))) - self._txrx_command(0x31) - self._txrx_command(struct.pack('>I', start_address)) - self._txrx_command(bytes([len(data) - 1, *data])) - - @retries - def erase_all(self): - logger(__name__).info('STM32: Erase all pages') - if not self.commands[6] == 0x43: - raise VEXCommError('Standard erase not supported on this device (only extended erase)') - self._txrx_command(0x43) - self._txrx_command(0xff) - - @retries - def erase_memory(self, page_numbers: List[int]): - logger(__name__).info(f'STM32: Erase pages: {page_numbers}') - if not self.commands[6] == 0x43: - raise VEXCommError('Standard erase not supported on this device (only extended erase)') - assert 0 < len(page_numbers) <= 255 - assert all([0 <= p <= 255 for p in page_numbers]) - self._txrx_command(0x43) - self._txrx_command(bytes([len(page_numbers) - 1, *page_numbers])) - - @retries - def extended_erase(self, page_numbers: List[int]): - logger(__name__).info(f'STM32: Extended Erase pages: {page_numbers}') - if not self.commands[6] == 0x44: - raise IOError('Extended erase not supported on this device (only standard erase)') - assert 0 < len(page_numbers) < 0xfff0 - assert all([0 <= p <= 0xffff for p in page_numbers]) - self._txrx_command(0x44) - self._txrx_command(bytes([len(page_numbers) - 1, *struct.pack(f'>{len(page_numbers)}H', *page_numbers)])) - - @retries - def extended_erase_special(self, command: int): - logger(__name__).info(f'STM32: Extended special erase: {command:x}') - if not self.commands[6] == 0x44: - raise IOError('Extended erase not supported on this device (only standard erase)') - assert 0xfffd <= command <= 0xffff - self._txrx_command(0x44) - self._txrx_command(struct.pack('>H', command)) - - def _txrx_command(self, command: Union[int, bytes], timeout: float = 0.01, checksum: bool = True): - self.port.read_all() - if isinstance(command, bytes): - message = command + (bytes([reduce(operator.xor, command, 0x00)]) if checksum else bytes([])) - elif isinstance(command, int): - message = bytearray([command, ~command & 0xff] if checksum else [command]) - else: - raise ValueError(f'Expected command to be bytes or int but got {type(command)}') - logger(__name__).debug(f'STM32 TX: {bytes_to_str(message)}') - self.port.write(message) - self.port.flush() - start_time = time.time() - while time.time() - start_time < timeout: - data = self.port.read(1) - if data and len(data) == 1: - logger(__name__).debug(f'STM32 RX: {data[0]} =?= {self.ACK_BYTE}') - if data[0] == self.ACK_BYTE: - return - raise VEXCommError(f"Device never ACK'd to {command}", command) diff --git a/pros/serial/devices/vex/v5_device.py b/pros/serial/devices/vex/v5_device.py deleted file mode 100644 index 2720c0c1..00000000 --- a/pros/serial/devices/vex/v5_device.py +++ /dev/null @@ -1,1036 +0,0 @@ -import gzip -import io -import re -import struct -import time -import typing -import platform -from collections import defaultdict -from configparser import ConfigParser -from datetime import datetime, timedelta -from enum import IntEnum, IntFlag -from io import BytesIO, StringIO -from pathlib import Path -from typing import * -from typing import BinaryIO - -from semantic_version import Spec - -from pros.common import ui -from pros.common import * -from pros.common.utils import * -from pros.conductor import Project -from pros.serial import bytes_to_str, decode_bytes_to_str -from pros.serial.ports import BasePort, list_all_comports -from .comm_error import VEXCommError -from .crc import CRC -from .message import Message -from .vex_device import VEXDevice -from ..system_device import SystemDevice - -int_str = Union[int, str] - - -def find_v5_ports(p_type: str): - def filter_vex_ports(p): - return p.vid is not None and p.vid in [0x2888, 0x0501] or \ - p.name is not None and ('VEX' in p.name or 'V5' in p.name) - - def filter_v5_ports(p, locations, names): - return (p.location is not None and any([p.location.endswith(l) for l in locations])) or \ - (p.name is not None and any([n in p.name for n in names])) or \ - (p.description is not None and any([n in p.description for n in names])) - - def filter_v5_ports_mac(p, device): - return (p.device is not None and p.device.endswith(device)) - - ports = [p for p in list_all_comports() if filter_vex_ports(p)] - - # Initially try filtering based off of location or the name of the device. - # Special logic for macOS - if platform.system() == 'Darwin': - user_ports = [p for p in ports if filter_v5_ports_mac(p, '3')] - system_ports = [p for p in ports if filter_v5_ports_mac(p, '1')] - joystick_ports = [p for p in ports if filter_v5_ports_mac(p, '2')] - else: - user_ports = [p for p in ports if filter_v5_ports(p, ['2'], ['User'])] - system_ports = [p for p in ports if filter_v5_ports(p, ['0'], ['System', 'Communications'])] - joystick_ports = [p for p in ports if filter_v5_ports(p, ['1'], ['Controller'])] - - # Fallback for when a brain port's location is not detected properly - if len(user_ports) != len(system_ports): - if len(user_ports) > len(system_ports): - system_ports = [p for p in ports if p not in user_ports and p not in joystick_ports] - else: - user_ports = [p for p in ports if p not in system_ports and p not in joystick_ports] - - if len(user_ports) == len(system_ports) and len(user_ports) > 0: - if p_type.lower() == 'user': - return user_ports - elif p_type.lower() == 'system': - return system_ports + joystick_ports - else: - raise ValueError(f'Invalid port type specified: {p_type}') - - # None of the typical filters worked, so if there are only two ports, then the lower one is always* - # the USER? port (*always = I haven't found a guarantee) - if len(ports) == 2: - # natural sort based on: https://stackoverflow.com/a/16090640 - def natural_key(chunk: str): - return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', chunk)] - - ports = sorted(ports, key=lambda p: natural_key(p.device)) - if p_type.lower() == 'user': - return [ports[1]] - elif p_type.lower() == 'system': - # check if ports contain the word Brain in the description and return that port - for port in ports: - if "Brain" in port.description: - return [port] - return [ports[0], *joystick_ports] - else: - raise ValueError(f'Invalid port type specified: {p_type}') - # these can now also be used as user ports - if len(joystick_ports) > 0: # and p_type.lower() == 'system': - return joystick_ports - return [] - - -def with_download_channel(f): - """ - Function decorator for use inside V5Device class. Needs to be outside the class because @staticmethod prevents - us from making a function decorator - """ - - def wrapped(device, *args, **kwargs): - with V5Device.DownloadChannel(device): - return f(device, *args, **kwargs) - - return wrapped - - -def compress_file(file: BinaryIO, file_len: int, label='Compressing binary') -> Tuple[BinaryIO, int]: - buf = io.BytesIO() - with ui.progressbar(length=file_len, label=label) as progress: - with gzip.GzipFile(fileobj=buf, mode='wb', mtime=0) as f: - while True: - data = file.read(16 * 1024) - if not data: - break - f.write(data) - progress.update(len(data)) - # recompute file length - file_len = buf.seek(0, 2) - buf.seek(0, 0) - return buf, file_len - - -class V5Device(VEXDevice, SystemDevice): - vid_map = {'user': 1, 'system': 15, 'rms': 16, 'pros': 24, 'mw': 32} # type: Dict[str, int] - channel_map = {'pit': 0, 'download': 1} # type: Dict[str, int] - - class FTCompleteOptions(IntEnum): - DONT_RUN = 0 - RUN_IMMEDIATELY = 0b01 - RUN_SCREEN = 0b11 - - VEX_CRC16 = CRC(16, 0x1021) # CRC-16-CCIT - VEX_CRC32 = CRC(32, 0x04C11DB7) # CRC-32 (the one used everywhere but has no name) - - class SystemVersion(object): - class Product(IntEnum): - CONTROLLER = 0x11 - BRAIN = 0x10 - - class BrainFlags(IntFlag): - CONNECTED = 0x02 - - class ControllerFlags(IntFlag): - CONNECTED = 0x02 - - flag_map = {Product.BRAIN: BrainFlags, Product.CONTROLLER: ControllerFlags} - - def __init__(self, data: tuple): - from semantic_version import Version - self.system_version = Version('{}.{}.{}-{}.{}'.format(*data[0:5])) - self.product = V5Device.SystemVersion.Product(data[5]) - self.product_flags = self.flag_map[self.product](data[6]) - - def __str__(self): - return f'System Version: {self.system_version}\n' \ - f' Product: {self.product.name}\n' \ - f' Product Flags: {self.product_flags.value:x}' - - class SystemStatus(object): - def __init__(self, data: tuple): - from semantic_version import Version - self.system_version = Version('{}.{}.{}-{}'.format(*data[0:4])) - self.cpu0_version = Version('{}.{}.{}-{}'.format(*data[4:8])) - self.cpu1_version = Version('{}.{}.{}-{}'.format(*data[8:12])) - self.touch_version = data[12] - self.system_id = data[13] - - def __getitem__(self, item): - return self.__dict__[item] - - def __init__(self, port: BasePort): - self._status = None - self._serial_cache = b'' - super().__init__(port) - - class DownloadChannel(object): - def __init__(self, device: 'V5Device', timeout: float = 5.): - self.device = device - self.timeout = timeout - self.did_switch = False - - def __enter__(self): - version = self.device.query_system_version() - if version.product == V5Device.SystemVersion.Product.CONTROLLER: - self.device.default_timeout = 2. - if V5Device.SystemVersion.ControllerFlags.CONNECTED not in version.product_flags: - raise VEXCommError('V5 Controller doesn\'t appear to be connected to a V5 Brain', version) - ui.echo('Transferring V5 to download channel') - self.device.ft_transfer_channel('download') - self.did_switch = True - logger(__name__).debug('Sleeping for a while to let V5 start channel transfer') - time.sleep(.25) # wait at least 250ms before starting to poll controller if it's connected yet - version = self.device.query_system_version() - start_time = time.time() - # ask controller every 250 ms if it's connected until it is - while V5Device.SystemVersion.ControllerFlags.CONNECTED not in version.product_flags and \ - time.time() - start_time < self.timeout: - version = self.device.query_system_version() - time.sleep(0.25) - if V5Device.SystemVersion.ControllerFlags.CONNECTED not in version.product_flags: - raise VEXCommError('Could not transfer V5 Controller to download channel', version) - logger(__name__).info('V5 should been transferred to higher bandwidth download channel') - return self - else: - return self - - def __exit__(self, *exc): - if self.did_switch: - self.device.ft_transfer_channel('pit') - ui.echo('V5 has been transferred back to pit channel') - - @property - def status(self): - if not self._status: - self._status = self.get_system_status() - return self._status - - @property - def can_compress(self): - return self.status['system_version'] in Spec('>=1.0.5') - - @property - def is_wireless(self): - version = self.query_system_version() - return version.product == V5Device.SystemVersion.Product.CONTROLLER and \ - V5Device.SystemVersion.ControllerFlags.CONNECTED in version.product_flags - - def generate_cold_hash(self, project: Project, extra: dict): - keys = {k: t.version for k, t in project.templates.items()} - keys.update(extra) - from hashlib import md5 - from base64 import b64encode - msg = str(sorted(keys, key=lambda t: t[0])).encode('ascii') - name = b64encode(md5(msg).digest()).rstrip(b'=').decode('ascii') - if Spec('<=1.0.0-27').match(self.status['cpu0_version']): - # Bug prevents linked files from being > 18 characters long. - # 17 characters is probably good enough for hash, so no need to fail out - name = name[:17] - return name - - def upload_project(self, project: Project, **kwargs): - assert project.target == 'v5' - monolith_path = project.location.joinpath(project.output) - if monolith_path.exists(): - logger(__name__).debug(f'Monolith exists! ({monolith_path})') - if 'hot_output' in project.templates['kernel'].metadata and \ - 'cold_output' in project.templates['kernel'].metadata: - hot_path = project.location.joinpath(project.templates['kernel'].metadata['hot_output']) - cold_path = project.location.joinpath(project.templates['kernel'].metadata['cold_output']) - upload_hot_cold = False - if hot_path.exists() and cold_path.exists(): - logger(__name__).debug(f'Hot and cold files exist! ({hot_path}; {cold_path})') - if monolith_path.exists(): - monolith_mtime = monolith_path.stat().st_mtime - hot_mtime = hot_path.stat().st_mtime - logger(__name__).debug(f'Monolith last modified: {monolith_mtime}') - logger(__name__).debug(f'Hot last modified: {hot_mtime}') - if hot_mtime > monolith_mtime: - upload_hot_cold = True - logger(__name__).debug('Hot file is newer than monolith!') - else: - upload_hot_cold = True - if upload_hot_cold: - with hot_path.open(mode='rb') as hot: - with cold_path.open(mode='rb') as cold: - kwargs['linked_file'] = cold - kwargs['linked_remote_name'] = self.generate_cold_hash(project, {}) - kwargs['linked_file_addr'] = int( - project.templates['kernel'].metadata.get('cold_addr', 0x03800000)) - kwargs['addr'] = int(project.templates['kernel'].metadata.get('hot_addr', 0x07800000)) - return self.write_program(hot, **kwargs) - if not monolith_path.exists(): - raise ui.dont_send(Exception('No output files were found! Have you built your project?')) - with monolith_path.open(mode='rb') as pf: - return self.write_program(pf, **kwargs) - - def generate_ini_file(self, remote_name: str = None, slot: int = 0, ini: ConfigParser = None, **kwargs): - project_ini = ConfigParser() - from semantic_version import Spec - default_icon = 'USER902x.bmp' if Spec('>=1.0.0-22').match(self.status['cpu0_version']) else 'USER999x.bmp' - project_ini['project'] = { - 'version': str(kwargs.get('ide_version') or get_version()), - 'ide': str(kwargs.get('ide') or 'PROS') - } - project_ini['program'] = { - 'version': kwargs.get('version', '0.0.0') or '0.0.0', - 'name': remote_name, - 'slot': slot, - 'icon': kwargs.get('icon', default_icon) or default_icon, - 'description': kwargs.get('description', 'Created with PROS'), - 'date': datetime.now().isoformat() - } - if ini: - project_ini.update(ini) - with StringIO() as ini_str: - project_ini.write(ini_str) - logger(__name__).info(f'Created ini: {ini_str.getvalue()}') - return ini_str.getvalue() - - @with_download_channel - def write_program(self, file: typing.BinaryIO, remote_name: str = None, ini: ConfigParser = None, slot: int = 0, - file_len: int = -1, run_after: FTCompleteOptions = FTCompleteOptions.DONT_RUN, - target: str = 'flash', quirk: int = 0, linked_file: Optional[typing.BinaryIO] = None, - linked_remote_name: Optional[str] = None, linked_file_addr: Optional[int] = None, - compress_bin: bool = True, **kwargs): - with ui.Notification(): - action_string = f'Uploading program "{remote_name}"' - finish_string = f'Finished uploading "{remote_name}"' - if hasattr(file, 'name'): - action_string += f' ({remote_name if remote_name else Path(file.name).name})' - finish_string += f' ({remote_name if remote_name else Path(file.name).name})' - action_string += f' to V5 slot {slot + 1} on {self.port}' - if compress_bin: - action_string += ' (compressed)' - ui.echo(action_string) - remote_base = f'slot_{slot + 1}' - if target == 'ddr': - self.write_file(file, f'{remote_base}.bin', file_len=file_len, type='bin', - target='ddr', run_after=run_after, linked_filename=linked_remote_name, **kwargs) - return - if not isinstance(ini, ConfigParser): - ini = ConfigParser() - if not remote_name: - remote_name = file.name - if len(remote_name) > 23: - logger(__name__).info('Truncating remote name to {} for length.'.format(remote_name[:20])) - remote_name = remote_name[:23] - - ini_file = self.generate_ini_file(remote_name=remote_name, slot=slot, ini=ini, **kwargs) - logger(__name__).info(f'Created ini: {ini_file}') - - if linked_file is not None: - self.upload_library(linked_file, remote_name=linked_remote_name, addr=linked_file_addr, - compress=compress_bin, force_upload=kwargs.pop('force_upload_linked', False)) - bin_kwargs = {k: v for k, v in kwargs.items() if v in ['addr']} - if (quirk & 0xff) == 1: - # WRITE BIN FILE - self.write_file(file, f'{remote_base}.bin', file_len=file_len, type='bin', run_after=run_after, - linked_filename=linked_remote_name, compress=compress_bin, **bin_kwargs, **kwargs) - with BytesIO(ini_file.encode(encoding='ascii')) as ini_bin: - # WRITE INI FILE - self.write_file(ini_bin, f'{remote_base}.ini', type='ini', **kwargs) - elif (quirk & 0xff) == 0: - # STOP PROGRAM - self.execute_program_file('', run=False) - with BytesIO(ini_file.encode(encoding='ascii')) as ini_bin: - # WRITE INI FILE - self.write_file(ini_bin, f'{remote_base}.ini', type='ini', **kwargs) - # WRITE BIN FILE - self.write_file(file, f'{remote_base}.bin', file_len=file_len, type='bin', run_after=run_after, - linked_filename=linked_remote_name, compress=compress_bin, **bin_kwargs, **kwargs) - else: - raise ValueError(f'Unknown quirk option: {quirk}') - ui.finalize('upload', f'{finish_string} to V5') - - def ensure_library_space(self, name: Optional[str] = None, vid: int_str = None, - target_name: Optional[str] = None): - """ - Uses algorithms, for loops, and if statements to determine what files should be removed - - This method searches for any orphaned files: - - libraries without any user files linking to it - - user files whose link does not exist - and removes them without prompt - - It will also ensure that only 3 libraries are being used on the V5. - If there are more than 3 libraries, then the oldest libraries are elected for eviction after a prompt. - "oldest" is determined by the most recently uploaded library or program linking to that library - """ - assert not (vid is None and name is not None) - used_libraries = [] - if vid is not None: - if isinstance(vid, str): - vid = self.vid_map[vid.lower()] - # assume all libraries - unused_libraries = [ - (vid, l['filename']) - for l - in [self.get_file_metadata_by_idx(i) - for i in range(0, self.get_dir_count(vid=vid)) - ] - ] - if name is not None: - if (vid, name) in unused_libraries: - # we'll be overwriting the library anyway, so remove it as a candidate for removal - unused_libraries.remove((vid, name)) - used_libraries.append((vid, name)) - else: - unused_libraries = [] - - programs: Dict[str, Dict] = { - # need the linked file metadata, so we have to use the get_file_metadata_by_name command - p['filename']: self.get_file_metadata_by_name(p['filename'], vid='user') - for p - in [self.get_file_metadata_by_idx(i) - for i in range(0, self.get_dir_count(vid='user'))] - if p['type'] == 'bin' - } - library_usage: Dict[Tuple[int, str], List[str]] = defaultdict(list) - for program_name, metadata in programs.items(): - library_usage[(metadata['linked_vid'], metadata['linked_filename'])].append(program_name) - - orphaned_files: List[Union[str, Tuple[int, str]]] = [] - for link, program_names in library_usage.items(): - linked_vid, linked_name = link - if name is not None and linked_vid == vid and linked_name == name: - logger(__name__).debug(f'{program_names} will be removed because the library will be replaced') - orphaned_files.extend(program_names) - elif linked_vid != 0: # linked_vid == 0 means there's no link. Can't be orphaned if there's no link - if link in unused_libraries: - # the library is being used - logger(__name__).debug(f'{link} is being used') - unused_libraries.remove(link) - used_libraries.append(link) - else: - try: - self.get_file_metadata_by_name(linked_name, vid=linked_vid) - logger(__name__).debug(f'{link} exists') - used_libraries.extend(link) - except VEXCommError as e: - logger(__name__).debug(dont_send(e)) - logger(__name__).debug(f'{program_names} will be removed because {link} does not exist') - orphaned_files.extend(program_names) - orphaned_files.extend(unused_libraries) - if target_name is not None and target_name in orphaned_files: - # the file will be overwritten anyway - orphaned_files.remove(target_name) - if len(orphaned_files) > 0: - logger(__name__).warning(f'Removing {len(orphaned_files)} orphaned file(s) ({orphaned_files})') - for file in orphaned_files: - if isinstance(file, tuple): - self.erase_file(file_name=file[1], vid=file[0]) - else: - self.erase_file(file_name=file, erase_all=True, vid='user') - - if len(used_libraries) > 3: - libraries = [ - (linked_vid, linked_name, self.get_file_metadata_by_name(linked_name, vid=linked_vid)['timestamp']) - for linked_vid, linked_name - in used_libraries - ] - library_usage_timestamps = sorted([ - ( - linked_vid, - linked_name, - # get the most recent timestamp of the library and all files linking to it - max(linked_timestamp, *[programs[p]['timestamp'] for p in library_usage[(linked_vid, linked_name)]]) - ) - for linked_vid, linked_name, linked_timestamp - in libraries - ], key=lambda t: t[2]) - evicted_files: List[Union[str, Tuple[int, str]]] = [] - evicted_file_list = '' - for evicted_library in library_usage_timestamps[:3]: - evicted_files.append(evicted_library[0:2]) - evicted_files.extend(library_usage[evicted_library[0:2]]) - evicted_file_list += evicted_library[1] + ', ' - evicted_file_list += ', '.join(library_usage[evicted_file_list[0:2]]) - evicted_file_list = evicted_file_list[:2] # remove last ", " - assert len(evicted_files) > 0 - if confirm(f'There are too many files on the V5. PROS can remove the following suggested old files: ' - f'{evicted_file_list}', - title='Confirm file eviction plan:'): - for file in evicted_files: - if isinstance(file, tuple): - self.erase_file(file_name=file[1], vid=file[0]) - else: - self.erase_file(file_name=file, erase_all=True, vid='user') - - def upload_library(self, file: typing.BinaryIO, remote_name: str = None, file_len: int = -1, vid: int_str = 'pros', - force_upload: bool = False, compress: bool = True, **kwargs): - """ - Upload a file used for linking. Contains the logic to check if the file is already present in the filesystem - and to prompt the user if we need to evict a library (and user programs). - - If force_upload is true, then skips the "is already present in the filesystem check" - """ - if not remote_name: - remote_name = file.name - if len(remote_name) > 23: - logger(__name__).info('Truncating remote name to {} for length.'.format(remote_name[:23])) - remote_name = remote_name[:23] - - if file_len < 0: - file_len = file.seek(0, 2) - file.seek(0, 0) - - if compress and self.can_compress: - file, file_len = compress_file(file, file_len, label='Compressing library') - - crc32 = self.VEX_CRC32.compute(file.read(file_len)) - file.seek(0, 0) - - if not force_upload: - try: - response = self.get_file_metadata_by_name(remote_name, vid) - logger(__name__).debug(response) - logger(__name__).debug({'file len': file_len, 'crc': crc32}) - if response['size'] == file_len and response['crc'] == crc32: - ui.echo('Library is already onboard V5') - return - else: - logger(__name__).warning(f'Library onboard doesn\'t match! ' - f'Length was {response["size"]} but expected {file_len} ' - f'CRC: was {response["crc"]:x} but expected {crc32:x}') - except VEXCommError as e: - logger(__name__).debug(e) - else: - logger(__name__).info('Skipping already-uploaded checks') - - logger(__name__).debug('Going to worry about uploading the file now') - self.ensure_library_space(remote_name, vid, ) - self.write_file(file, remote_name, file_len, vid=vid, **kwargs) - - def read_file(self, file: typing.IO[bytes], remote_file: str, vid: int_str = 'user', target: int_str = 'flash', - addr: Optional[int] = None, file_len: Optional[int] = None): - if isinstance(vid, str): - vid = self.vid_map[vid.lower()] - if addr is None: - metadata = self.get_file_metadata_by_name(remote_file, vid=vid) - addr = metadata['addr'] - wireless = self.is_wireless - ft_meta = self.ft_initialize(remote_file, function='download', vid=vid, target=target, addr=addr) - if file_len is None: - file_len = ft_meta['file_size'] - - if wireless and file_len > 0x25000: - confirm(f'You\'re about to download {file_len} bytes wirelessly. This could take some time, and you should ' - f'consider downloading directly with a wire.', abort=True, default=False) - - max_packet_size = ft_meta['max_packet_size'] - with ui.progressbar(length=file_len, label='Downloading {}'.format(remote_file)) as progress: - for i in range(0, file_len, max_packet_size): - packet_size = max_packet_size - if i + max_packet_size > file_len: - packet_size = file_len - i - file.write(self.ft_read(addr + i, packet_size)) - progress.update(packet_size) - logger(__name__).debug('Completed {} of {} bytes'.format(i + packet_size, file_len)) - self.ft_complete() - - def write_file(self, file: typing.BinaryIO, remote_file: str, file_len: int = -1, - run_after: FTCompleteOptions = FTCompleteOptions.DONT_RUN, linked_filename: Optional[str] = None, - linked_vid: int_str = 'pros', compress: bool = False, **kwargs): - if file_len < 0: - file_len = file.seek(0, 2) - file.seek(0, 0) - display_name = remote_file - if hasattr(file, 'name'): - display_name = f'{remote_file} ({Path(file.name).name})' - if compress and self.can_compress: - file, file_len = compress_file(file, file_len) - - if self.is_wireless and file_len > 0x25000: - confirm(f'You\'re about to upload {file_len} bytes wirelessly. This could take some time, and you should ' - f'consider uploading directly with a wire.', abort=True, default=False) - crc32 = self.VEX_CRC32.compute(file.read(file_len)) - file.seek(0, 0) - addr = kwargs.get('addr', 0x03800000) - logger(__name__).info('Transferring {} ({} bytes) to the V5 from {}'.format(remote_file, file_len, file)) - ft_meta = self.ft_initialize(remote_file, function='upload', length=file_len, crc=crc32, **kwargs) - if linked_filename is not None: - logger(__name__).debug('Setting file link') - self.ft_set_link(linked_filename, vid=linked_vid) - assert ft_meta['file_size'] >= file_len - if len(remote_file) > 24: - logger(__name__).info('Truncating {} to {} due to length'.format(remote_file, remote_file[:24])) - remote_file = remote_file[:24] - max_packet_size = int(ft_meta['max_packet_size'] / 2) - with ui.progressbar(length=file_len, label='Uploading {}'.format(display_name)) as progress: - for i in range(0, file_len, max_packet_size): - packet_size = max_packet_size - if i + max_packet_size > file_len: - packet_size = file_len - i - logger(__name__).debug('Writing {} bytes at 0x{:02X}'.format(packet_size, addr + i)) - self.ft_write(addr + i, file.read(packet_size)) - progress.update(packet_size) - logger(__name__).debug('Completed {} of {} bytes'.format(i + packet_size, file_len)) - logger(__name__).debug('Data transfer complete, sending ft complete') - if compress and self.status['system_version'] in Spec('>=1.0.5'): - logger(__name__).info('Closing gzip file') - file.close() - self.ft_complete(options=run_after) - - @with_download_channel - def capture_screen(self) -> Tuple[List[List[int]], int, int]: - self.sc_init() - width, height = 512, 272 - file_size = width * height * 4 # ARGB - - rx_io = BytesIO() - self.read_file(rx_io, '', vid='system', target='screen', addr=0, file_len=file_size) - rx = rx_io.getvalue() - rx = struct.unpack('<{}I'.format(len(rx) // 4), rx) - - data = [[] for _ in range(height)] - for y in range(height): - for x in range(width - 1): - if x < 480: - px = rx[y * width + x] - data[y].append((px & 0xff0000) >> 16) - data[y].append((px & 0x00ff00) >> 8) - data[y].append(px & 0x0000ff) - - return data, 480, height - - def used_slots(self) -> Dict[int, Optional[str]]: - with ui.Notification(): - rv = {} - for slot in range(1, 9): - ini = self.read_ini(f'slot_{slot}.ini') - rv[slot] = ini['program']['name'] if ini is not None else None - return rv - - def read_ini(self, remote_name: str) -> Optional[ConfigParser]: - try: - rx_io = BytesIO() - self.read_file(rx_io, remote_name) - config = ConfigParser() - rx_io.seek(0, 0) - config.read_string(rx_io.read().decode('ascii')) - return config - except VEXCommError as e: - return None - - @retries - def query_system_version(self) -> SystemVersion: - logger(__name__).debug('Sending simple 0xA408 command') - ret = self._txrx_simple_struct(0xA4, '>8B') - logger(__name__).debug('Completed simple 0xA408 command') - return V5Device.SystemVersion(ret) - - @retries - def ft_transfer_channel(self, channel: int_str): - logger(__name__).debug(f'Transferring to {channel} channel') - logger(__name__).debug('Sending ext 0x10 command') - if isinstance(channel, str): - channel = self.channel_map[channel] - assert isinstance(channel, int) and 0 <= channel <= 1 - self._txrx_ext_packet(0x10, struct.pack('<2B', 1, channel), rx_length=0) - logger(__name__).debug('Completed ext 0x10 command') - - @retries - def ft_initialize(self, file_name: str, **kwargs) -> Dict[str, Any]: - logger(__name__).debug('Sending ext 0x11 command') - options = { - 'function': 'upload', - 'target': 'flash', - 'vid': 'user', - 'overwrite': True, - 'options': 0, - 'length': 0, - 'addr': 0x03800000, - 'crc': 0, - 'type': 'bin', - 'timestamp': datetime.now(), - 'version': 0x01_00_00_00, - 'name': file_name - } - options.update({k: v for k, v in kwargs.items() if k in options and v is not None}) - - if isinstance(options['function'], str): - options['function'] = {'upload': 1, 'download': 2}[options['function'].lower()] - if isinstance(options['target'], str): - options['target'] = {'ddr': 0, 'flash': 1, 'screen': 2}[options['target'].lower()] - if isinstance(options['vid'], str): - options['vid'] = self.vid_map[options['vid'].lower()] - if isinstance(options['type'], str): - options['type'] = options['type'].encode(encoding='ascii') - if isinstance(options['name'], str): - options['name'] = options['name'].encode(encoding='ascii') - options['options'] |= 1 if options['overwrite'] else 0 - options['timestamp'] = int((options['timestamp'] - datetime(2000, 1, 1)).total_seconds()) - - logger(__name__).debug('Initializing file transfer w/: {}'.format(options)) - tx_payload = struct.pack("<4B3I4s2I24s", options['function'], options['target'], options['vid'], - options['options'], options['length'], options['addr'], options['crc'], - options['type'], options['timestamp'], options['version'], options['name']) - rx = self._txrx_ext_struct(0x11, tx_payload, " bytearray: - logger(__name__).debug('Sending ext 0x14 command') - actual_n_bytes = n_bytes + (0 if n_bytes % 4 == 0 else 4 - n_bytes % 4) - ui.logger(__name__).debug(dict(actual_n_bytes=actual_n_bytes, addr=addr)) - tx_payload = struct.pack(" int: - logger(__name__).debug('Sending ext 0x16 command') - if isinstance(vid, str): - vid = self.vid_map[vid.lower()] - tx_payload = struct.pack("<2B", vid, options) - ret = self._txrx_ext_struct(0x16, tx_payload, " Dict[str, Any]: - logger(__name__).debug('Sending ext 0x17 command') - tx_payload = struct.pack("<2B", file_idx, options) - rx = self._txrx_ext_struct(0x17, tx_payload, " Dict[str, Any]: - logger(__name__).debug('Sending ext 0x19 command') - if isinstance(vid, str): - vid = self.vid_map[vid.lower()] - ui.logger(__name__).debug(f'Options: {dict(vid=vid, file_name=file_name)}') - tx_payload = struct.pack("<2B24s", vid, options, file_name.encode(encoding='ascii')) - rx = self._txrx_ext_struct(0x19, tx_payload, " Dict[str, Any]: - logger(__name__).debug('Sending ext 0x1C command') - tx_payload = struct.pack("<2B24s", vid, options, file_name.encode(encoding='ascii')) - ret = self._txrx_ext_struct(0x1C, tx_payload, " SystemStatus: - from semantic_version import Version - logger(__name__).debug('Sending ext 0x22 command') - version = self.query_system_version() - if (version.product == V5Device.SystemVersion.Product.BRAIN and version.system_version in Spec('<1.0.13')) or \ - (version.product == V5Device.SystemVersion.Product.CONTROLLER and version.system_version in Spec('<1.0.0-0.70')): - schema = ' bytes: - # I can't really think of a better way to only return when a full - # COBS message was written than to just cache the data until we hit a \x00. - - # read/write are the same command, behavior dictated by specifying - # length-to-read as 0xFF and providing additional payload bytes to write or - # specifying a length-to-read and no additional data to read. - logger(__name__).debug('Sending ext 0x27 command (read)') - # specifying a length to read (0x40 bytes) with no additional payload data. - tx_payload = struct.pack("<2B", self.channel_map['download'], 0x40) - # RX length isn't always 0x40 (end of buffer reached), so don't check_length. - self._serial_cache += self._txrx_ext_packet(0x27, tx_payload, 0, check_length=False)[1:] - logger(__name__).debug('Completed ext 0x27 command (read)') - # if _serial_cache doesn't have a \x00, pretend we didn't read anything. - if b'\x00' not in self._serial_cache: - return b'' - # _serial_cache has a \x00, split off the beginning part and hand it down. - parts = self._serial_cache.split(b'\x00') - ret = parts[0] + b'\x00' - self._serial_cache = b'\x00'.join(parts[1:]) - - return ret - - @retries - def user_fifo_write(self, payload: Union[Iterable, bytes, bytearray, str]): - # Not currently implemented - return - logger(__name__).debug('Sending ext 0x27 command (write)') - max_packet_size = 224 - pl_len = len(payload) - for i in range(0, pl_len, max_packet_size): - packet_size = max_packet_size - if i + max_packet_size > pl_len: - packet_size = pl_len - i - logger(__name__).debug(f'Writing {packet_size} bytes to user FIFO') - self._txrx_ext_packet(0x27, b'\x01\x00' + payload[i:packet_size], 0, check_length=False)[1:] - logger(__name__).debug('Completed ext 0x27 command (write)') - - @retries - def sc_init(self) -> None: - """ - Send command to initialize screen capture - """ - # This will only copy data in memory, not send! - logger(__name__).debug('Sending ext 0x28 command') - self._txrx_ext_struct(0x28, [], '') - logger(__name__).debug('Completed ext 0x28 command') - - @retries - def kv_read(self, kv: str) -> bytearray: - logger(__name__).debug('Sending ext 0x2e command') - encoded_kv = f'{kv}\0'.encode(encoding='ascii') - tx_payload = struct.pack(f'<{len(encoded_kv)}s', encoded_kv) - # Because the length of the kernel variables is not known, use None to indicate we are recieving an unknown length. - ret = self._txrx_ext_packet(0x2e, tx_payload, 1, check_length=False, check_ack=True) - logger(__name__).debug('Completed ext 0x2e command') - return ret - - @retries - def kv_write(self, kv: str, payload: Union[Iterable, bytes, bytearray, str]): - logger(__name__).debug('Sending ext 0x2f command') - encoded_kv = f'{kv}\0'.encode(encoding='ascii') - kv_to_max_bytes = { - 'teamnumber': 7, - 'robotname': 16 - } - if len(payload) > kv_to_max_bytes.get(kv, 254): - print(f'Truncating input to meet maximum value length ({kv_to_max_bytes[kv]} characters).') - # Trim down size of payload to fit within the 255 byte limit and add null terminator. - payload = payload[:kv_to_max_bytes.get(kv, 254)] + "\0" - if isinstance(payload, str): - payload = payload.encode(encoding='ascii') - tx_fmt = f'<{len(encoded_kv)}s{len(payload)}s' - tx_payload = struct.pack(tx_fmt, encoded_kv, payload) - ret = self._txrx_ext_packet(0x2f, tx_payload, 1, check_length=False, check_ack=True) - logger(__name__).debug('Completed ext 0x2f command') - return payload - - def _txrx_ext_struct(self, command: int, tx_data: Union[Iterable, bytes, bytearray], - unpack_fmt: str, check_length: bool = True, check_ack: bool = True, - timeout: Optional[float] = None) -> Tuple: - """ - Transmits and receives an extended command to the V5, automatically unpacking the values according to unpack_fmt - which gets passed into struct.unpack. The size of the payload is determined from the fmt string - :param command: Extended command code - :param tx_data: Transmission payload - :param unpack_fmt: Format to expect the raw payload to be in - :param retries: Number of retries to attempt to parse the output before giving up - :param rx_wait: Amount of time to wait after transmitting the packet before reading the response - :param check_ack: If true, then checks the first byte of the extended payload as an AK byte - :return: A tuple unpacked according to the unpack_fmt - """ - rx = self._txrx_ext_packet(command, tx_data, struct.calcsize(unpack_fmt), - check_length=check_length, check_ack=check_ack, timeout=timeout) - logger(__name__).debug('Unpacking with format: {}'.format(unpack_fmt)) - return struct.unpack(unpack_fmt, rx) - - @classmethod - def _rx_ext_packet(cls, msg: Message, command: int, rx_length: int, check_ack: bool = True, - check_length: bool = True) -> Message: - """ - Parse a received packet - :param msg: data to parse - :param command: The extended command sent - :param rx_length: Expected length of the received data - :param check_ack: If true, checks the first byte as an AK byte - :param tx_payload: what was sent, used if an exception needs to be thrown - :return: The payload of the extended message - """ - assert (msg['command'] == 0x56) - if not cls.VEX_CRC16.compute(msg.rx) == 0: - raise VEXCommError("CRC of message didn't match 0: {}".format(cls.VEX_CRC16.compute(msg.rx)), msg) - assert (msg['payload'][0] == command) - msg = msg['payload'][1:-2] - if check_ack: - nacks = { - 0xFF: "General NACK", - 0xCE: "CRC error on recv'd packet", - 0xD0: "Payload too small", - 0xD1: "Request transfer size too large", - 0xD2: "Program CRC error", - 0xD3: "Program file error", - 0xD4: "Attempted to download/upload uninitialized", - 0xD5: "Initialization invalid for this function", - 0xD6: "Data not a multiple of 4 bytes", - 0xD7: "Packet address does not match expected", - 0xD8: "Data downloaded does not match initial length", - 0xD9: "Directory entry does not exist", - 0xDA: "Max user files, no more room for another user program", - 0xDB: "User file exists" - } - if msg[0] in nacks.keys(): - raise VEXCommError("Device NACK'd with reason: {}".format(nacks[msg[0]]), msg) - elif msg[0] != cls.ACK_BYTE: - raise VEXCommError("Device didn't ACK", msg) - msg = msg[1:] - if len(msg) > 0: - logger(cls).debug('Set msg window to {}'.format(bytes_to_str(msg))) - if len(msg) < rx_length and check_length: - raise VEXCommError(f'Received length is less than {rx_length} (got {len(msg)}).', msg) - elif len(msg) > rx_length and check_length: - ui.echo( - f'WARNING: Recieved length is more than {rx_length} (got {len(msg)}). Consider upgrading the PROS (CLI Version: {get_version()}).') - return msg - - def _txrx_ext_packet(self, command: int, tx_data: Union[Iterable, bytes, bytearray], - rx_length: int, check_length: bool = True, - check_ack: bool = True, timeout: Optional[float] = None) -> Message: - """ - Transmits and receives an extended command to the V5. - :param command: Extended command code - :param tx_data: Tranmission payload - :param rx_length: Expected length of the received extended payload - :param rx_wait: Amount of time to wait after transmitting the packet before reading the response - :param check_ack: If true, then checks the first byte of the extended payload as an AK byte - :return: A bytearray of the extended payload - """ - tx_payload = self._form_extended_payload(command, tx_data) - rx = self._txrx_packet(0x56, tx_data=tx_payload, timeout=timeout) - - return self._rx_ext_packet(rx, command, rx_length, check_ack=check_ack, check_length=check_length) - - @classmethod - def _form_extended_payload(cls, msg: int, payload: Union[Iterable, bytes, bytearray]) -> bytearray: - if payload is None: - payload = bytearray() - payload_length = len(payload) - assert payload_length <= 0x7f_ff - if payload_length >= 0x80: - payload_length = [(payload_length >> 8) | 0x80, payload_length & 0xff] - else: - payload_length = [payload_length] - packet = bytearray([msg, *payload_length, *payload]) - crc = cls.VEX_CRC16.compute(bytes([*cls._form_simple_packet(0x56), *packet])) - packet = bytearray([*packet, crc >> 8, crc & 0xff]) - assert (cls.VEX_CRC16.compute(bytes([*cls._form_simple_packet(0x56), *packet])) == 0) - return packet diff --git a/pros/serial/devices/vex/v5_user_device.py b/pros/serial/devices/vex/v5_user_device.py deleted file mode 100644 index be40d6b4..00000000 --- a/pros/serial/devices/vex/v5_user_device.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import * - -from cobs import cobs -from pros.common.utils import logger -from pros.serial.devices.stream_device import StreamDevice -from pros.serial.ports import BasePort - - -class V5UserDevice(StreamDevice): - def __init__(self, port: BasePort): - super().__init__(port) - self.topics: Set[bytes] = set() - self._accept_all = False - self.buffer: bytearray = bytearray() - - def subscribe(self, topic: bytes): - self.topics.add(topic) - - def unsubscribe(self, topic: bytes): - self.topics.remove(topic) - - @property - def promiscuous(self): - return self._accept_all - - @promiscuous.setter - def promiscuous(self, value: bool): - self._accept_all = True - - def write(self, data: Union[str, bytes]): - if isinstance(data, str): - data = data.encode(encoding='ascii') - self.port.write(data) - - def read(self) -> Tuple[bytes, bytes]: - msg = None, None - while msg[0] is None or (msg[0] not in self.topics and not self._accept_all): - while b'\0' not in self.buffer: - self.buffer.extend(self.port.read(1)) - self.buffer.extend(self.port.read(-1)) - assert b'\0' in self.buffer - msg, self.buffer = self.buffer.split(b'\0', 1) - try: - msg = cobs.decode(msg) - except cobs.DecodeError: - logger(__name__).warning(f'Could not decode bytes: {msg.hex()}') - assert len(msg) >= 4 - msg = bytes(msg[:4]), bytes(msg[4:]) - return msg diff --git a/pros/serial/devices/vex/vex_device.py b/pros/serial/devices/vex/vex_device.py deleted file mode 100644 index ff9862d4..00000000 --- a/pros/serial/devices/vex/vex_device.py +++ /dev/null @@ -1,124 +0,0 @@ -import struct -import time -from typing import * - -from pros.common import * -from pros.serial import bytes_to_str -from pros.serial.ports import BasePort -from . import comm_error -from .message import Message -from ..generic_device import GenericDevice - - -def debug(msg): - print(msg) - - -class VEXDevice(GenericDevice): - ACK_BYTE = 0x76 - NACK_BYTE = 0xFF - - def __init__(self, port: BasePort, timeout=0.1): - super().__init__(port) - self.default_timeout = timeout - - @retries - def query_system(self) -> bytearray: - """ - Verify that a VEX device is connected. Returned payload varies by product - :return: Payload response - """ - logger(__name__).debug('Sending simple 0x21 command') - return self._txrx_simple_packet(0x21, 0x0A) - - def _txrx_simple_struct(self, command: int, unpack_fmt: str, timeout: Optional[float] = None) -> Tuple: - rx = self._txrx_simple_packet(command, struct.calcsize(unpack_fmt), timeout=timeout) - return struct.unpack(unpack_fmt, rx) - - def _txrx_simple_packet(self, command: int, rx_len: int, timeout: Optional[float] = None) -> bytearray: - """ - Transmits a simple command to the VEX device, performs the standard quality of message checks, then - returns the payload. - Will check if the received command matches the sent command and the received length matches the expected length - :param command: Command to send to the device - :param rx_len: Expected length of the received message - :return: They payload of the message, or raises and exception if there was an issue - """ - msg = self._txrx_packet(command, timeout=timeout) - if msg['command'] != command: - raise comm_error.VEXCommError('Received command does not match sent command.', msg) - if len(msg['payload']) != rx_len: - raise comm_error.VEXCommError("Received data doesn't match expected length", msg) - return msg['payload'] - - def _rx_packet(self, timeout: Optional[float] = None) -> Dict[str, Union[Union[int, bytes, bytearray], Any]]: - # Optimized to read as quickly as possible w/o delay - start_time = time.time() - response_header = bytes([0xAA, 0x55]) - response_header_stack = list(response_header) - rx = bytearray() - if timeout is None: - timeout = self.default_timeout - while (len(rx) > 0 or time.time() - start_time < timeout) and len(response_header_stack) > 0: - b = self.port.read(1) - if len(b) == 0: - continue - b = b[0] - if b == response_header_stack[0]: - response_header_stack.pop(0) - rx.append(b) - else: - logger(__name__).debug("Tossing rx ({}) because {} didn't match".format(bytes_to_str(rx), b)) - response_header_stack = bytearray(response_header) - rx = bytearray() - if not rx == bytearray(response_header): - raise IOError(f"Couldn't find the response header in the device response after {timeout} s. " - f"Got {rx.hex()} but was expecting {response_header.hex()}") - rx.extend(self.port.read(1)) - command = rx[-1] - rx.extend(self.port.read(1)) - payload_length = rx[-1] - if command == 0x56 and (payload_length & 0x80) == 0x80: - logger(__name__).debug('Found an extended message payload') - rx.extend(self.port.read(1)) - payload_length = ((payload_length & 0x7f) << 8) + rx[-1] - payload = self.port.read(payload_length) - rx.extend(payload) - return { - 'command': command, - 'payload': payload, - 'raw': rx - } - - def _tx_packet(self, command: int, tx_data: Union[Iterable, bytes, bytearray, None] = None): - tx = self._form_simple_packet(command) - if tx_data is not None: - tx = bytes([*tx, *tx_data]) - logger(__name__).debug(f'{self.__class__.__name__} TX: {bytes_to_str(tx)}') - self.port.read_all() - self.port.write(tx) - self.port.flush() - return tx - - def _txrx_packet(self, command: int, tx_data: Union[Iterable, bytes, bytearray, None] = None, - timeout: Optional[float] = None) -> Message: - """ - Goes through a send/receive cycle with a VEX device. - Transmits the command with the optional additional payload, then reads and parses the outer layer - of the response - :param command: Command to send the device - :param tx_data: Optional extra data to send the device - :return: Returns a dictionary containing the received command field and the payload. Correctly computes the - payload length even if the extended command (0x56) is used (only applies to the V5). - """ - tx = self._tx_packet(command, tx_data) - rx = self._rx_packet(timeout=timeout) - msg = Message(rx['raw'], tx) - logger(__name__).debug(msg) - msg['payload'] = Message(rx['raw'], tx, internal_rx=rx['payload']) - msg['command'] = rx['command'] - return msg - - @staticmethod - def _form_simple_packet(msg: int) -> bytearray: - return bytearray([0xc9, 0x36, 0xb8, 0x47, msg]) diff --git a/pros/serial/interactive/UploadProjectModal.py b/pros/serial/interactive/UploadProjectModal.py deleted file mode 100644 index f14dde7e..00000000 --- a/pros/serial/interactive/UploadProjectModal.py +++ /dev/null @@ -1,170 +0,0 @@ -import os.path -import time -from threading import Thread -from typing import * - -import pros.common.ui as ui -from pros.common.ui.interactive import application, components, parameters -from pros.common.utils import with_click_context -from pros.conductor import Project -from pros.conductor.interactive import ExistingProjectParameter -from pros.serial.devices.vex import find_cortex_ports, find_v5_ports -from pros.serial.ports import list_all_comports - - -class UploadProjectModal(application.Modal[None]): - def __init__(self, project: Optional[Project]): - super(UploadProjectModal, self).__init__('Upload Project', confirm_button='Upload') - - self.project: Optional[Project] = project - self.project_path = ExistingProjectParameter( - str(project.location) if project else os.path.join(os.path.expanduser('~'), 'My PROS Project') - ) - - self.port = parameters.OptionParameter('', ['']) - self.save_settings = parameters.BooleanParameter(True) - self.advanced_options: Dict[str, parameters.Parameter] = {} - self.advanced_options_collapsed = parameters.BooleanParameter(True) - - self.alive = True - self.poll_comports_thread: Optional[Thread] = None - - @self.on_exit - def cleanup_poll_comports_thread(): - if self.poll_comports_thread is not None and self.poll_comports_thread.is_alive(): - self.alive = False - self.poll_comports_thread.join() - - cb = self.project_path.on_changed(self.project_changed, asynchronous=True) - if self.project_path.is_valid(): - cb(self.project_path) - - def update_slots(self): - assert self.project.target == 'v5' - if self.port.is_valid() and bool(self.port.value): - from pros.serial.devices.vex import V5Device - from pros.serial.ports import DirectPort - device = V5Device(DirectPort(self.port.value)) - slot_options = [ - f'{slot}' + ('' if program is None else f' (Currently: {program})') - for slot, program in - device.used_slots().items() - ] - else: - slot_options = [str(i) for i in range(1, 9)] - project_name = self.advanced_options['name'].value - if 'slot' in self.project.upload_options: - # first, see if the project has it specified in its upload options - selected = slot_options[self.project.upload_options['slot'] - 1] - else: - # otherwise, try to do a name match - matched_slots = [i for i, slot in enumerate(slot_options) if slot.endswith(f'{project_name})')] - if len(matched_slots) > 0: - selected = slot_options[matched_slots[0]] - elif 'slot' in self.advanced_options: - # or whatever the last value was - selected = slot_options[int(self.advanced_options['slot'].value[0]) - 1] - else: - # or just slot 1 - selected = slot_options[0] - self.advanced_options['slot'] = parameters.OptionParameter( - selected, slot_options - ) - - def update_comports(self): - list_all_comports.cache_clear() - - if isinstance(self.project, Project): - options = {} - if self.project.target == 'v5': - options = {p.device for p in find_v5_ports('system')} - elif self.project.target == 'cortex': - options = [p.device for p in find_cortex_ports()] - if options != {*self.port.options}: - self.port.options = list(options) - if self.port.value not in options: - self.port.update(self.port.options[0] if len(self.port.options) > 0 else 'No ports found') - ui.logger(__name__).debug('Updating ports') - - if self.project and self.project.target == 'v5': - self.update_slots() - - self.redraw() - - def poll_comports(self): - while self.alive: - self.update_comports() - time.sleep(2) - - def project_changed(self, new_project: ExistingProjectParameter): - try: - self.project = Project(new_project.value) - - assert self.project is not None - - if self.project.target == 'v5': - self.advanced_options = { - 'name': parameters.Parameter(self.project.upload_options.get('remote_name', self.project.name)), - 'description': parameters.Parameter( - self.project.upload_options.get('description', 'Created with PROS') - ), - 'compress_bin': parameters.BooleanParameter( - self.project.upload_options.get('compress_bin', True) - ) - } - self.update_slots() - else: - self.advanced_options = {} - - self.update_comports() - - self.redraw() - except BaseException as e: - ui.logger(__name__).exception(e) - - def confirm(self, *args, **kwargs): - from pros.cli.upload import upload - from click import get_current_context - kwargs = {'path': None, 'project': self.project, 'port': self.port.value} - savable_kwargs = {} - if self.project.target == 'v5': - savable_kwargs['remote_name'] = self.advanced_options['name'].value - # XXX: the first character is the slot number - savable_kwargs['slot'] = int(self.advanced_options['slot'].value[0]) - savable_kwargs['description'] = self.advanced_options['description'].value - savable_kwargs['compress_bin'] = self.advanced_options['compress_bin'].value - - if self.save_settings.value: - self.project.upload_options.update(savable_kwargs) - self.project.save() - - kwargs.update(savable_kwargs) - self.exit() - get_current_context().invoke(upload, **kwargs) - - @property - def can_confirm(self): - advanced_valid = all( - p.is_valid() - for p in self.advanced_options.values() - if isinstance(p, parameters.ValidatableParameter) - ) - return self.project is not None and self.port.is_valid() and advanced_valid - - def build(self) -> Generator[components.Component, None, None]: - if self.poll_comports_thread is None: - self.poll_comports_thread = Thread(target=with_click_context(self.poll_comports)) - self.poll_comports_thread.start() - - yield components.DirectorySelector('Project Directory', self.project_path) - yield components.DropDownBox('Port', self.port) - yield components.Checkbox('Save upload settings', self.save_settings) - - if isinstance(self.project, Project) and self.project.target == 'v5': - yield components.Container( - components.InputBox('Program Name', self.advanced_options['name']), - components.DropDownBox('Slot', self.advanced_options['slot']), - components.InputBox('Description', self.advanced_options['description']), - components.Checkbox('Compress Binary', self.advanced_options['compress_bin']), - title='Advanced V5 Options', - collapsed=self.advanced_options_collapsed) diff --git a/pros/serial/interactive/__init__.py b/pros/serial/interactive/__init__.py deleted file mode 100644 index aa7f4062..00000000 --- a/pros/serial/interactive/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .UploadProjectModal import UploadProjectModal - -__all__ = ['UploadProjectModal'] diff --git a/pros/serial/ports/__init__.py b/pros/serial/ports/__init__.py deleted file mode 100644 index be344a79..00000000 --- a/pros/serial/ports/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from functools import lru_cache - -from pros.common import logger -from serial.tools import list_ports as list_ports - -from .base_port import BasePort, PortConnectionException, PortException -from .direct_port import DirectPort -# from .v5_wireless_port import V5WirelessPort - - -@lru_cache() -def list_all_comports(): - ports = list_ports.comports() - logger(__name__).debug('Connected: {}'.format(';'.join([str(p.__dict__) for p in ports]))) - return ports diff --git a/pros/serial/ports/base_port.py b/pros/serial/ports/base_port.py deleted file mode 100644 index 6bfc03fc..00000000 --- a/pros/serial/ports/base_port.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import * - - -class BasePort(object): - def write(self, data: bytes): - raise NotImplementedError() - - def read(self, n_bytes: int = 0) -> bytes: - raise NotImplementedError() - - def read_all(self): - return self.read() - - def config(self, command: str, argument: Any): - pass - - def flush_input(self): - pass - - def flush_output(self): - pass - - def destroy(self): - pass - - def flush(self): - self.flush_output() - self.flush_input() - - @property - def name(self) -> str: - raise NotImplementedError - - -class PortException(IOError): - pass - - -class PortConnectionException(PortException): - pass diff --git a/pros/serial/ports/direct_port.py b/pros/serial/ports/direct_port.py deleted file mode 100644 index fa225f54..00000000 --- a/pros/serial/ports/direct_port.py +++ /dev/null @@ -1,73 +0,0 @@ -import sys -from typing import * - -import serial - -from pros.common import logger, dont_send -from pros.serial.ports.exceptions import ConnectionRefusedException, PortNotFoundException -from .base_port import BasePort, PortConnectionException - - -def create_serial_port(port_name: str, timeout: Optional[float] = 1.0) -> serial.Serial: - try: - logger(__name__).debug(f'Opening serial port {port_name}') - port = serial.Serial(port_name, baudrate=115200, bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) - port.timeout = timeout - port.inter_byte_timeout = 0.2 - return port - except serial.SerialException as e: - if any(msg in str(e) for msg in [ - 'Access is denied', 'Errno 16', 'Errno 13' - ]): - tb = sys.exc_info()[2] - raise dont_send(ConnectionRefusedException(port_name, e).with_traceback(tb)) - else: - raise dont_send(PortNotFoundException(port_name, e)) - - - -class DirectPort(BasePort): - - def __init__(self, port_name: str, **kwargs): - self.serial: serial.Serial = create_serial_port(port_name=port_name, timeout=kwargs.pop('timeout', 1.0)) - self.buffer: bytearray = bytearray() - - def read(self, n_bytes: int = 0) -> bytes: - try: - if n_bytes <= 0: - self.buffer.extend(self.serial.read_all()) - msg = bytes(self.buffer) - self.buffer = bytearray() - return msg - else: - if len(self.buffer) < n_bytes: - self.buffer.extend(self.serial.read(n_bytes - len(self.buffer))) - if len(self.buffer) < n_bytes: - msg = bytes(self.buffer) - self.buffer = bytearray() - else: - msg, self.buffer = bytes(self.buffer[:n_bytes]), self.buffer[n_bytes:] - return msg - except serial.SerialException as e: - logger(__name__).debug(e) - raise PortConnectionException(e) - - def write(self, data: Union[str, bytes]): - if isinstance(data, str): - data = data.encode(encoding='ascii') - self.serial.write(data) - - def flush(self): - self.serial.flush() - - def destroy(self): - logger(__name__).debug(f'Destroying {self.__class__.__name__} to {self.serial.name}') - self.serial.close() - - @property - def name(self) -> str: - return self.serial.portstr - - def __str__(self): - return str(self.serial.port) diff --git a/pros/serial/ports/exceptions.py b/pros/serial/ports/exceptions.py deleted file mode 100644 index cd3f0bca..00000000 --- a/pros/serial/ports/exceptions.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import serial - -class ConnectionRefusedException(IOError): - def __init__(self, port_name: str, reason: Exception): - self.__cause__ = reason - self.port_name = port_name - - def __str__(self): - extra = '' - if os.name == 'posix': - extra = 'adding yourself to dialout group ' - return f"could not open port '{self.port_name}'. Try closing any other VEX IDEs such as VEXCode, Robot Mesh Studio, or " \ - f"firmware utilities; moving to a different USB port; {extra}or " \ - f"restarting the device." - -class PortNotFoundException(serial.SerialException): - def __init__(self, port_name: str, reason: Exception): - self.__cause__ = reason - self.port_name = port_name - - def __str__(self): - extra = '' - if os.name == 'posix': - extra = 'adding yourself to dialout group ' - return f"Port not found: Could not open port '{self.port_name}'. Try closing any other VEX IDEs such as VEXCode, Robot Mesh Studio, or " \ - f"firmware utilities; moving to a different USB port; {extra}or " \ - f"restarting the device." - - diff --git a/pros/serial/ports/serial_share_bridge.py b/pros/serial/ports/serial_share_bridge.py deleted file mode 100644 index b632a5dc..00000000 --- a/pros/serial/ports/serial_share_bridge.py +++ /dev/null @@ -1,177 +0,0 @@ -import logging.handlers -import multiprocessing -import threading -import time - -import zmq -from cobs import cobs -from pros.common.utils import * - -from .direct_port import DirectPort -from .. import bytes_to_str - - -def get_port_num(serial_port_name: str, hash: str) -> int: - return sum("Powered by PROS: {}-{}".format(serial_port_name, hash).encode(encoding='ascii')) - - -def get_from_device_port_num(serial_port_name: str) -> int: - return get_port_num(serial_port_name, 'from') - - -def get_to_device_port_num(serial_port_name: str) -> int: - return get_port_num(serial_port_name, 'to') - - -class SerialShareBridge(object): - def __init__(self, serial_port_name: str, base_addr: str = '127.0.0.1', - to_device_port_num: int = None, from_device_port_num: int = None): - self._serial_port_name = serial_port_name - self._base_addr = base_addr - if to_device_port_num is None: - to_device_port_num = get_to_device_port_num(serial_port_name) - if from_device_port_num is None: - from_device_port_num = get_from_device_port_num(serial_port_name) - self._to_port_num = to_device_port_num - self._from_port_num = from_device_port_num - self.port = None # type: SerialPort - self.zmq_ctx = None # type: zmq.Context - self.from_device_thread = None # type: threading.Thread - self.to_device_thread = None # type: threading.Thread - self.dying = None # type: threading.Event - - @property - def to_device_port_num(self): - return self._to_port_num - - @property - def from_device_port_num(self): - return self._from_port_num - - def start(self): - # this function is still in the parent process - mp_ctx = multiprocessing.get_context('spawn') - barrier = multiprocessing.Barrier(3) - task = mp_ctx.Process(target=self._start, name='Serial Share Bridge', args=(barrier,)) - task.daemon = False - task.start() - barrier.wait(1) - return task - - def kill(self, do_join: bool = False): - logger(__name__).info('Killing serial share server due to watchdog') - self.dying.set() - self.port.destroy() - if not self.zmq_ctx.closed: - self.zmq_ctx.destroy(linger=0) - if do_join: - if threading.current_thread() != self.from_device_thread and self.from_device_thread.is_alive(): - self.from_device_thread.join() - if threading.current_thread() != self.to_device_thread and self.to_device_thread.is_alive(): - self.to_device_thread.join() - - def _start(self, initialization_barrier: multiprocessing.Barrier): - try: - log_dir = os.path.join(get_pros_dir(), 'logs') - os.makedirs(log_dir, exist_ok=True) - pros_logger = logging.getLogger(pros.__name__) - pros_logger.setLevel(logging.DEBUG) - log_file_name = os.path.join(get_pros_dir(), 'logs', 'serial-share-bridge.log') - handler = logging.handlers.TimedRotatingFileHandler(log_file_name, backupCount=1) - handler.setLevel(logging.DEBUG) - fmt_str = '%(name)s.%(funcName)s:%(levelname)s - %(asctime)s - %(message)s (%(process)d) ({})' \ - .format(self._serial_port_name) - handler.setFormatter(logging.Formatter(fmt_str)) - pros_logger.addHandler(handler) - - self.zmq_ctx = zmq.Context() - # timeout is none, so blocks indefinitely. Helps reduce CPU usage when there's nothing being recv - self.port = DirectPort(self._serial_port_name, timeout=None) - self.from_device_thread = threading.Thread(target=self._from_device_loop, name='From Device Reader', - daemon=False, args=(initialization_barrier,)) - self.to_device_thread = threading.Thread(target=self._to_device_loop, name='To Device Reader', - daemon=False, args=(initialization_barrier,)) - self.dying = threading.Event() # type: threading.Event - self.from_device_thread.start() - self.to_device_thread.start() - - while not self.dying.wait(10000): - pass - - logger(__name__).info('Main serial share bridge thread is dying. Everything else should be dead: {}'.format( - threading.active_count() - 1)) - self.kill(do_join=True) - except Exception as e: - initialization_barrier.abort() - logger(__name__).exception(e) - - def _from_device_loop(self, initialization_barrier: multiprocessing.Barrier): - errors = 0 - rxd = 0 - try: - from_ser_sock = self.zmq_ctx.socket(zmq.PUB) - addr = 'tcp://{}:{}'.format(self._base_addr, self._from_port_num) - from_ser_sock.bind(addr) - logger(__name__).info('Bound from device broadcaster as a publisher to {}'.format(addr)) - initialization_barrier.wait() - buffer = bytearray() - while not self.dying.is_set(): - try: - # read one byte as a blocking call so that we aren't just polling which sucks up a lot of CPU, - # then read everything available - buffer.extend(self.port.read(1)) - buffer.extend(self.port.read(-1)) - while b'\0' in buffer and not self.dying.is_set(): - msg, buffer = buffer.split(b'\0', 1) - msg = cobs.decode(msg) - from_ser_sock.send_multipart((msg[:4], msg[4:])) - rxd += 1 - time.sleep(0) - except Exception as e: - # TODO: when getting a COBS decode error, rebroadcast the bytes on sout - logger(__name__).error('Unexpected error handling {}'.format(bytes_to_str(msg[:-1]))) - logger(__name__).exception(e) - errors += 1 - logger(__name__).info('Current from device broadcasting error rate: {} errors. {} successful. {}%' - .format(errors, rxd, errors / (errors + rxd))) - except Exception as e: - initialization_barrier.abort() - logger(__name__).exception(e) - logger(__name__).warning('From Device Broadcaster is dying now.') - logger(__name__).info('Current from device broadcasting error rate: {} errors. {} successful. {}%' - .format(errors, rxd, errors / (errors + rxd))) - try: - self.kill(do_join=False) - except: - sys.exit(0) - - def _to_device_loop(self, initialization_barrier: multiprocessing.Barrier): - try: - to_ser_sock = self.zmq_ctx.socket(zmq.SUB) - addr = 'tcp://{}:{}'.format(self._base_addr, self._to_port_num) - to_ser_sock.bind(addr) - to_ser_sock.setsockopt(zmq.SUBSCRIBE, b'') - logger(__name__).info('Bound to device broadcaster as a subscriber to {}'.format(addr)) - watchdog = threading.Timer(10, self.kill) - initialization_barrier.wait() - watchdog.start() - while not self.dying.is_set(): - msg = to_ser_sock.recv_multipart() - if not msg or self.dying.is_set(): - continue - if msg[0] == b'kick': - logger(__name__).debug('Kicking watchdog on server {}'.format(threading.current_thread())) - watchdog.cancel() - watchdog = threading.Timer(msg[1][1] if len(msg) > 1 and len(msg[1]) > 0 else 5, self.kill) - watchdog.start() - elif msg[0] == b'send': - logger(self).debug('Writing {} to {}'.format(bytes_to_str(msg[1]), self.port.port_name)) - self.port.write(msg[1]) - except Exception as e: - initialization_barrier.abort() - logger(__name__).exception(e) - logger(__name__).warning('To Device Broadcaster is dying now.') - try: - self.kill(do_join=False) - except: - sys.exit(0) diff --git a/pros/serial/ports/serial_share_port.py b/pros/serial/ports/serial_share_port.py deleted file mode 100644 index f329ac7e..00000000 --- a/pros/serial/ports/serial_share_port.py +++ /dev/null @@ -1,83 +0,0 @@ -from .base_port import BasePort -from .serial_share_bridge import * - - -class SerialSharePort(BasePort): - def __init__(self, port_name: str, topic: bytes = b'sout', addr: str = '127.0.0.1', - to_device_port: int = None, from_device_port: int = None): - self.port_name = port_name - self.topic = topic - self._base_addr = addr - self._to_port_num = to_device_port - self._from_port_num = from_device_port - - if self._to_port_num is None: - self._to_port_num = get_to_device_port_num(self.port_name) - if self._from_port_num is None: - self._from_port_num = get_from_device_port_num(self.port_name) - - server = SerialShareBridge(self.port_name, self._base_addr, self._to_port_num, self._from_port_num) - server.start() - - self.ctx = zmq.Context() # type: zmq.Context - - self.from_device_sock = self.ctx.socket(zmq.SUB) # type: zmq.Socket - self.from_device_sock.setsockopt(zmq.SUBSCRIBE, self.topic) - self.from_device_sock.setsockopt(zmq.SUBSCRIBE, b'kdbg') - self.from_device_sock.connect('tcp://{}:{}'.format(self._base_addr, self._from_port_num)) - logger(__name__).info( - 'Connected from device as a subscriber on tcp://{}:{}'.format(self._base_addr, self._from_port_num)) - - self.to_device_sock = self.ctx.socket(zmq.PUB) # type: zmq.Socket - self.to_device_sock.connect('tcp://{}:{}'.format(self._base_addr, self._to_port_num)) - logger(__name__).info( - 'Connected to device as a publisher on tcp://{}:{}'.format(self._base_addr, self._to_port_num)) - - self.alive = threading.Event() - self.watchdog_thread = threading.Thread(target=self._kick_watchdog, name='Client Kicker') - self.watchdog_thread.start() - - def read(self, n_bytes: int = -1): - if n_bytes <= 0: - n_bytes = 1 - data = bytearray() - for _ in range(n_bytes): - data.extend(self.from_device_sock.recv_multipart()[1]) - return bytes(data) - - def read_packet(self): - return self.from_device_sock.recv_multipart() - - def write(self, data: AnyStr): - if isinstance(data, str): - data = data.encode(encoding='ascii') - assert isinstance(data, bytes) - self.to_device_sock.send_multipart([b'send', data]) - - def subscribe(self, topic: bytes): - assert len(topic) == 4 - self.write(bytearray([*b'pRe', *topic])) - self.from_device_sock.subscribe(topic=topic) - - def unsubscribe(self, topic: bytes): - assert len(topic) == 4 - self.write(bytearray([*b'pRd', *topic])) - self.from_device_sock.unsubscribe(topic=topic) - - def destroy(self): - logger(__name__).info('Destroying {}'.format(self)) - self.alive.set() - if self.watchdog_thread.is_alive(): - self.watchdog_thread.join() - if not self.from_device_sock.closed: - self.from_device_sock.close(linger=0) - if not self.ctx.closed: - self.ctx.destroy(linger=0) - - def _kick_watchdog(self): - time.sleep(0.5) - while not self.alive.is_set(): - logger(__name__).debug('Kicking server from {}'.format(threading.current_thread())) - self.to_device_sock.send_multipart([b'kick']) - self.alive.wait(2.5) - logger(__name__).info('Watchdog kicker is dying') diff --git a/pros/serial/ports/v5_wireless_port.py b/pros/serial/ports/v5_wireless_port.py deleted file mode 100644 index 80d4717d..00000000 --- a/pros/serial/ports/v5_wireless_port.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import * - -from pros.serial.devices.vex.v5_device import V5Device -from pros.serial.ports import BasePort, DirectPort - - -class V5WirelessPort(BasePort): - def __init__(self, port): - self.buffer: bytearray = bytearray() - - self.port_instance = DirectPort(port) - self.device = V5Device(self.port_instance) - self.download_channel = self.device.DownloadChannel(self.device) - self.download_channel.__enter__() - - def destroy(self): - self.port_instance.destroy() - self.download_channel.__exit__() - - def config(self, command: str, argument: Any): - return self.port_instance.config(command, argument) - - # TODO: buffer input? technically this is done by the user_fifo_write cmd blocking until whole input is written? - def write(self, data: bytes): - self.device.user_fifo_write(data) - - def read(self, n_bytes: int = 0) -> bytes: - if n_bytes > len(self.buffer): - self.buffer.extend(self.device.user_fifo_read()) - ret = self.buffer[:n_bytes] - self.buffer = self.buffer[n_bytes:] - return ret - - @property - def name(self) -> str: - return self.port_instance.name diff --git a/pros/serial/terminal/__init__.py b/pros/serial/terminal/__init__.py deleted file mode 100644 index a3b7b088..00000000 --- a/pros/serial/terminal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .terminal import Terminal diff --git a/pros/serial/terminal/terminal.py b/pros/serial/terminal/terminal.py deleted file mode 100644 index a0c78264..00000000 --- a/pros/serial/terminal/terminal.py +++ /dev/null @@ -1,302 +0,0 @@ -import codecs -import os -import signal -import sys -import threading - -import colorama - -from pros.common.utils import logger -from pros.serial import decode_bytes_to_str -from pros.serial.devices import StreamDevice -from pros.serial.ports import PortConnectionException - - -# This file is a modification of the miniterm implementation on pyserial - - -class ConsoleBase(object): - """OS abstraction for console (input/output codec, no echo)""" - - def __init__(self): - if sys.version_info >= (3, 0): - self.byte_output = sys.stdout.buffer - else: - self.byte_output = sys.stdout - self.output = sys.stdout - - def setup(self): - """Set console to read single characters, no echo""" - - def cleanup(self): - """Restore default console settings""" - - def getkey(self): - """Read a single key from the console""" - return None - - def write_bytes(self, byte_string): - """Write bytes (already encoded)""" - self.byte_output.write(byte_string) - self.byte_output.flush() - - def write(self, text): - """Write string""" - self.output.write(text) - self.output.flush() - - def cancel(self): - """Cancel getkey operation""" - - # - - - - - - - - - - - - - - - - - - - - - - - - - # context manager: - # switch terminal temporary to normal mode (e.g. to get user input) - - def __enter__(self): - self.cleanup() - return self - - def __exit__(self, *args, **kwargs): - self.setup() - - -if os.name == 'nt': # noqa - import msvcrt - import ctypes - - - class Out(object): - """file-like wrapper that uses os.write""" - - def __init__(self, fd): - self.fd = fd - - def flush(self): - pass - - def write(self, s): - os.write(self.fd, s) - - - class Console(ConsoleBase): - def __init__(self): - super(Console, self).__init__() - self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() - self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() - ctypes.windll.kernel32.SetConsoleOutputCP(65001) - ctypes.windll.kernel32.SetConsoleCP(65001) - self.output = sys.stdout - # self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), - # 'replace') - # the change of the code page is not propagated to Python, - # manually fix it - # sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), - # 'replace') - sys.stdout = self.output - # self.output.encoding = 'UTF-8' # needed for input - - def __del__(self): - ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) - ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) - - def getkey(self): - while True: - z = msvcrt.getwch() - if z == chr(13): - return chr(10) - elif z in (chr(0), chr(0x0e)): # functions keys, ignore - msvcrt.getwch() - else: - return z - - def cancel(self): - # CancelIo, CancelSynchronousIo do not seem to work when using - # getwch, so instead, send a key to the window with the console - hwnd = ctypes.windll.kernel32.GetConsoleWindow() - ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0) - -elif os.name == 'posix': - import atexit - import termios - import select - - - class Console(ConsoleBase): - def __init__(self): - super(Console, self).__init__() - self.fd = sys.stdin.fileno() - # an additional pipe is used in getkey, so that the cancel method - # can abort the waiting getkey method - self.pipe_r, self.pipe_w = os.pipe() - self.old = termios.tcgetattr(self.fd) - atexit.register(self.cleanup) - if sys.version_info < (3, 0): - self.enc_stdin = codecs. \ - getreader(sys.stdin.encoding)(sys.stdin) - else: - self.enc_stdin = sys.stdin - - def setup(self): - new = termios.tcgetattr(self.fd) - new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG - new[6][termios.VMIN] = 1 - new[6][termios.VTIME] = 0 - termios.tcsetattr(self.fd, termios.TCSANOW, new) - - def getkey(self): - ready, _, _ = select.select([self.enc_stdin, self.pipe_r], [], - [], None) - if self.pipe_r in ready: - os.read(self.pipe_r, 1) - return - c = self.enc_stdin.read(1) - if c == chr(0x7f): - c = chr(8) # map the BS key (which yields DEL) to backspace - return c - - def cancel(self): - os.write(self.pipe_w, b"x") - - def cleanup(self): - termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) - -else: - raise NotImplementedError( - 'Sorry no implementation for your platform ({})' - ' available.'.format(sys.platform)) - - -class Terminal(object): - """This class is loosely based off of the pyserial miniterm""" - - def __init__(self, port_instance: StreamDevice, transformations=(), - output_raw: bool = False, request_banner: bool = True): - self.device = port_instance - self.device.subscribe(b'sout') - self.device.subscribe(b'serr') - self.transformations = transformations - self._reader_alive = None - self.receiver_thread = None # type: threading.Thread - self._transmitter_alive = None - self.transmitter_thread = None # type: threading.Thread - self.alive = threading.Event() # type: threading.Event - self.output_raw = output_raw - self.request_banner = request_banner - self.no_sigint = True # SIGINT flag - signal.signal(signal.SIGINT, self.catch_sigint) # SIGINT handler - self.console = Console() - self.console.output = colorama.AnsiToWin32(self.console.output).stream - - def _start_rx(self): - self._reader_alive = True - self.receiver_thread = threading.Thread(target=self.reader, - name='serial-rx-term') - self.receiver_thread.daemon = True - self.receiver_thread.start() - - def _stop_rx(self): - self._reader_alive = False - self.receiver_thread.join() - - def _start_tx(self): - self._transmitter_alive = True - self.transmitter_thread = threading.Thread(target=self.transmitter, - name='serial-tx-term') - self.transmitter_thread.daemon = True - self.transmitter_thread.start() - - def _stop_tx(self): - self.console.cancel() - self._transmitter_alive = False - self.transmitter_thread.join() - - def reader(self): - if self.request_banner: - try: - self.device.write(b'pRb') - except Exception as e: - logger(__name__).exception(e) - try: - while not self.alive.is_set() and self._reader_alive: - data = self.device.read() - if not data: - continue - if data[0] == b'sout': - text = decode_bytes_to_str(data[1]) - elif data[0] == b'serr': - text = '{}{}{}'.format(colorama.Fore.RED, decode_bytes_to_str(data[1]), colorama.Style.RESET_ALL) - elif data[0] == b'kdbg': - text = '{}\n\nKERNEL DEBUG:\t{}{}\n'.format(colorama.Back.GREEN + colorama.Style.BRIGHT, - decode_bytes_to_str(data[1]), - colorama.Style.RESET_ALL) - elif data[0] != b'': - text = '{}{}'.format(decode_bytes_to_str(data[0]), decode_bytes_to_str(data[1])) - else: - text = "{}".format(decode_bytes_to_str(data[1])) - self.console.write(text) - except UnicodeError as e: - logger(__name__).exception(e) - except PortConnectionException: - logger(__name__).warning(f'Connection to {self.device.name} broken') - if not self.alive.is_set(): - self.stop() - except Exception as e: - if not self.alive.is_set(): - logger(__name__).exception(e) - else: - logger(__name__).debug(e) - self.stop() - logger(__name__).info('Terminal receiver dying') - - def transmitter(self): - try: - while not self.alive.is_set() and self._transmitter_alive: - try: - c = self.console.getkey() - except KeyboardInterrupt: - c = '\x03' - if self.alive.is_set(): - break - if c == '\x03' or not self.no_sigint: - self.stop() - break - else: - self.device.write(c.encode(encoding='utf-8')) - self.console.write(c) - except Exception as e: - if not self.alive.is_set(): - logger(__name__).exception(e) - else: - logger(__name__).debug(e) - self.stop() - logger(__name__).info('Terminal transmitter dying') - - def catch_sigint(self): - self.no_sigint = False - - def start(self): - self.console.setup() - self.alive.clear() - self._start_rx() - self._start_tx() - - # noinspection PyUnusedLocal - def stop(self, *args): - self.console.cleanup() - if not self.alive.is_set(): - logger(__name__).warning('Stopping terminal') - self.alive.set() - self.device.destroy() - if threading.current_thread() != self.transmitter_thread and self.transmitter_thread.is_alive(): - self.console.cleanup() - self.console.cancel() - logger(__name__).info('All done!') - - def join(self): - try: - if self.receiver_thread.is_alive(): - self.receiver_thread.join() - if self.transmitter_thread.is_alive(): - self.transmitter_thread.join() - except: - self.stop() diff --git a/pros/upgrade/__init__.py b/pros/upgrade/__init__.py deleted file mode 100644 index 9794ad32..00000000 --- a/pros/upgrade/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .upgrade_manager import UpgradeManager, UpgradeManifestV2 - - -def get_platformv2(): - return UpgradeManifestV2().platform - - -__all__ = ['UpgradeManager', 'get_platformv2'] diff --git a/pros/upgrade/instructions/__init__.py b/pros/upgrade/instructions/__init__.py deleted file mode 100644 index 26d62f32..00000000 --- a/pros/upgrade/instructions/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .base_instructions import UpgradeInstruction, UpgradeResult -from .nothing_instructions import NothingInstruction -from .download_instructions import DownloadInstruction -from .explorer_instructions import ExplorerInstruction - -__all__ = ['UpgradeInstruction', 'UpgradeResult', 'NothingInstruction', 'ExplorerInstruction', 'DownloadInstruction'] diff --git a/pros/upgrade/instructions/base_instructions.py b/pros/upgrade/instructions/base_instructions.py deleted file mode 100644 index 81f543fd..00000000 --- a/pros/upgrade/instructions/base_instructions.py +++ /dev/null @@ -1,19 +0,0 @@ -class UpgradeResult(object): - def __init__(self, successful: bool, **kwargs): - self.successful = successful - self.__dict__.update(**kwargs) - - def __str__(self): - return f'The upgrade was {"" if self.successful else "not "}successful.\n{getattr(self, "explanation", "")}' - - -class UpgradeInstruction(object): - """ - Base class for all upgrade instructions, not useful to instantiate - """ - - def perform_upgrade(self) -> UpgradeResult: - raise NotImplementedError() - - def __str__(self) -> str: - raise NotImplementedError() diff --git a/pros/upgrade/instructions/download_instructions.py b/pros/upgrade/instructions/download_instructions.py deleted file mode 100644 index 48f8b49e..00000000 --- a/pros/upgrade/instructions/download_instructions.py +++ /dev/null @@ -1,34 +0,0 @@ -import os.path -from typing import * - -from pros.common.utils import download_file -from .base_instructions import UpgradeInstruction, UpgradeResult - - -class DownloadInstruction(UpgradeInstruction): - """ - Downloads a file - """ - def __init__(self, url='', extension=None, download_description=None, success_explanation=None): - self.url: str = url - self.extension: Optional[str] = extension - self.download_description: Optional[str] = download_description - self.success_explanation: Optional[str] = success_explanation - - def perform_upgrade(self) -> UpgradeResult: - assert self.url - try: - file = download_file(self.url, ext=self.extension, desc=self.download_description) - assert file - except (AssertionError, IOError) as e: - return UpgradeResult(False, explanation=f'Failed to download required file. ({e})', exception=e) - - if self.success_explanation: - explanation = self.success_explanation.replace('//FILE\\\\', file) \ - .replace('//SHORT\\\\', os.path.split(file)[1]) - else: - explanation = f'Downloaded {os.path.split(file)[1]}' - return UpgradeResult(True, explanation=explanation, file=file, origin=self.url) - - def __str__(self) -> str: - return 'Download required file.' diff --git a/pros/upgrade/instructions/explorer_instructions.py b/pros/upgrade/instructions/explorer_instructions.py deleted file mode 100644 index ae843ba3..00000000 --- a/pros/upgrade/instructions/explorer_instructions.py +++ /dev/null @@ -1,18 +0,0 @@ -from .base_instructions import UpgradeResult -from .download_instructions import DownloadInstruction - - -class ExplorerInstruction(DownloadInstruction): - """ - Opens file explorer of the downloaded file - """ - - def perform_upgrade(self) -> UpgradeResult: - result = super().perform_upgrade() - if result.successful: - import click - click.launch(getattr(result, 'file')) - return result - - def __str__(self) -> str: - return 'Download required file.' diff --git a/pros/upgrade/instructions/nothing_instructions.py b/pros/upgrade/instructions/nothing_instructions.py deleted file mode 100644 index a3619173..00000000 --- a/pros/upgrade/instructions/nothing_instructions.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base_instructions import UpgradeInstruction, UpgradeResult - - -class NothingInstruction(UpgradeInstruction): - def __str__(self) -> str: - return 'No automated instructions. View release notes for installation instructions.' - - def perform_upgrade(self) -> UpgradeResult: - return UpgradeResult(True) diff --git a/pros/upgrade/manifests/__init__.py b/pros/upgrade/manifests/__init__.py deleted file mode 100644 index 290f42c5..00000000 --- a/pros/upgrade/manifests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import * - -from .upgrade_manifest_v1 import UpgradeManifestV1 -from .upgrade_manifest_v2 import UpgradeManifestV2, PlatformsV2 - -# Order of files -manifests = [UpgradeManifestV2, UpgradeManifestV1] # type: List[Type] -__all__ = ['UpgradeManifestV1', 'UpgradeManifestV2', 'manifests', 'PlatformsV2'] diff --git a/pros/upgrade/manifests/upgrade_manifest_v1.py b/pros/upgrade/manifests/upgrade_manifest_v1.py deleted file mode 100644 index 51ba9346..00000000 --- a/pros/upgrade/manifests/upgrade_manifest_v1.py +++ /dev/null @@ -1,47 +0,0 @@ -from semantic_version import Version - -from pros.common.utils import get_version, logger -from ..instructions import UpgradeResult - - -class UpgradeManifestV1(object): - """ - An Upgrade Manifest only capable of determine if there is an update - not how to update - """ - - def __init__(self): - self.version: Version = None - self.info_url: str = None - - @property - def needs_upgrade(self) -> bool: - """ - :return: True if the current CLI version is less than the upgrade manifest - """ - return self.version > Version(get_version()) - - def describe_update(self) -> str: - """ - Describes the update - :return: - """ - if self.needs_upgrade: - return f'There is an update available! {self.version} is the latest version.\n' \ - f'Go to {self.info_url} to learn more.' - else: - return f'You are up to date. ({self.version})' - - def __str__(self): - return self.describe_update() - - @property - def can_perform_upgrade(self) -> bool: - return isinstance(self.info_url, str) - - def perform_upgrade(self) -> UpgradeResult: - logger(__name__).debug(self.__dict__) - from click import launch - return UpgradeResult(launch(self.info_url) == 0) - - def describe_post_install(self, **kwargs) -> str: - return f'Download the latest version from {self.info_url}' diff --git a/pros/upgrade/manifests/upgrade_manifest_v2.py b/pros/upgrade/manifests/upgrade_manifest_v2.py deleted file mode 100644 index b024aa3d..00000000 --- a/pros/upgrade/manifests/upgrade_manifest_v2.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys -from enum import Enum -from typing import * - -from pros.common import logger -from .upgrade_manifest_v1 import UpgradeManifestV1 -from ..instructions import UpgradeInstruction, UpgradeResult, NothingInstruction - - -class PlatformsV2(Enum): - Unknown = 0 - Windows86 = 1 - Windows64 = 2 - MacOS = 3 - Linux = 4 - Pip = 5 - - -class UpgradeManifestV2(UpgradeManifestV1): - """ - an Upgrade Manifest capable of determining if there is an update, and possibly an installer to download, - but without the knowledge of how to run the installer - """ - - def __init__(self): - super().__init__() - self.platform_instructions: Dict[PlatformsV2, UpgradeInstruction] = {} - - self._platform: 'PlatformsV2' = None - - self._last_file: Optional[str] = None - - @property - def platform(self) -> 'PlatformsV2': - """ - Attempts to detect the current platform type - :return: The detected platform type, or Unknown - """ - if self._platform is not None: - return self._platform - if getattr(sys, 'frozen', False): - import _constants - frozen_platform = getattr(_constants, 'FROZEN_PLATFORM_V1', None) - if isinstance(frozen_platform, str): - if frozen_platform.startswith('Windows86'): - self._platform = PlatformsV2.Windows86 - elif frozen_platform.startswith('Windows64'): - self._platform = PlatformsV2.Windows64 - elif frozen_platform.startswith('MacOS'): - self._platform = PlatformsV2.MacOS - else: - try: - from pip._vendor import pkg_resources - results = [p for p in pkg_resources.working_set if p.project_name.startswith('pros-cli')] - if any(results): - self._platform = PlatformsV2.Pip - except ImportError: - pass - if not self._platform: - self._platform = PlatformsV2.Unknown - return self._platform - - @property - def can_perform_upgrade(self) -> bool: - return True - - def perform_upgrade(self) -> UpgradeResult: - instructions: UpgradeInstruction = self.platform_instructions.get(self.platform, NothingInstruction()) - logger(__name__).debug(self.__dict__) - logger(__name__).debug(f'Platform: {self.platform}') - logger(__name__).debug(instructions.__dict__) - return instructions.perform_upgrade() - - def __repr__(self): - return repr({ - 'platform': self.platform, - **self.__dict__ - }) diff --git a/pros/upgrade/upgrade_manager.py b/pros/upgrade/upgrade_manager.py deleted file mode 100644 index 3ddcf8eb..00000000 --- a/pros/upgrade/upgrade_manager.py +++ /dev/null @@ -1,96 +0,0 @@ -import os.path -from datetime import datetime -from enum import Enum -from typing import * - -from pros.common import logger -import pros.common.ui as ui -from pros.config import Config -from pros.config.cli_config import cli_config -from .manifests import * -from .instructions import UpgradeResult - - -class ReleaseChannel(Enum): - Stable = 'stable' - Beta = 'beta' - - -class UpgradeManager(Config): - def __init__(self, file=None): - if file is None: - file = os.path.join(cli_config().directory, 'upgrade.pros.json') - self._last_check: datetime = datetime.min - self._manifest: Optional[UpgradeManifestV1] = None - self.release_channel: ReleaseChannel = ReleaseChannel.Stable - - super().__init__(file) - - @property - def has_stale_manifest(self): - if self._manifest is None: - logger(__name__).debug('Upgrade manager\'s manifest is nonexistent') - if datetime.now() - self._last_check > cli_config().update_frequency: - logger(__name__).debug(f'Upgrade manager\'s last check occured at {self._last_check}.') - logger(__name__).debug(f'Was longer ago than update frequency ({cli_config().update_frequency}) allows.') - return (self._manifest is None) or (datetime.now() - self._last_check > cli_config().update_frequency) - - def get_manifest(self, force: bool = False) -> UpgradeManifestV1: - if not force and not self.has_stale_manifest: - return self._manifest - - ui.echo('Fetching upgrade manifest...') - import requests - import jsonpickle - import json - - channel_url = f'https://purduesigbots.github.io/pros-mainline/{self.release_channel.value}' - self._manifest = None - - manifest_urls = [f"{channel_url}/{manifest.__name__}.json" for manifest in manifests] - for manifest_url in manifest_urls: - resp = requests.get(manifest_url) - if resp.status_code == 200: - try: - self._manifest = jsonpickle.decode(resp.text, keys=True) - logger(__name__).debug(self._manifest) - self._last_check = datetime.now() - self.save() - break - except json.decoder.JSONDecodeError as e: - logger(__name__).warning(f'Failed to decode {manifest_url}') - logger(__name__).debug(e) - else: - logger(__name__).debug(f'Failed to get {manifest_url} ({resp.status_code})') - if not self._manifest: - manifest_list = "\n".join(manifest_urls) - logger(__name__).warning(f'Could not access any upgrade manifests from any of:\n{manifest_list}') - return self._manifest - - @property - def needs_upgrade(self) -> bool: - manifest = self.get_manifest() - if manifest is None: - return False - return manifest.needs_upgrade - - def describe_update(self) -> str: - manifest = self.get_manifest() - assert manifest is not None - return manifest.describe_update() - - @property - def can_perform_upgrade(self): - manifest = self.get_manifest() - assert manifest is not None - return manifest.can_perform_upgrade - - def perform_upgrade(self) -> UpgradeResult: - manifest = self.get_manifest() - assert manifest is not None - return manifest.perform_upgrade() - - def describe_post_upgrade(self) -> str: - manifest = self.get_manifest() - assert manifest is not None - return manifest.describe_post_install() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index beb4f05c..00000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -click>=8 -rich-click==1.7.4 -pyserial -cachetools -requests -requests-futures -tabulate -jsonpickle -semantic_version -colorama -pyzmq -cobs -scan-build==2.0.13 -sentry-sdk -observable -pypng==0.0.20 -pyinstaller diff --git a/setup.py b/setup.py deleted file mode 100644 index f26a9741..00000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -# setup.py for non-frozen builds - -from setuptools import setup, find_packages -from install_requires import install_requires as install_reqs - -setup( - name='pros-cli', - version=open('pip_version').read().strip(), - packages=find_packages(), - url='https://github.com/purduesigbots/pros-cli', - license='MPL-2.0', - author='Purdue ACM SIGBots', - author_email='pros_development@cs.purdue.edu', - description='Command Line Interface for managing PROS projects', - install_requires=install_reqs, - entry_points={ - 'console_scripts': [ - 'pros=pros.cli.main:main', - 'prosv5=pros.cli.main:main' - ] - } -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b89febd5..00000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pep8] -max-line-length = 120 - -[flake8] -max-line-length = 120 -ignore = F405, E722, E303, F403, E126 diff --git a/version b/version deleted file mode 100644 index e5b8a844..00000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -3.5.4 \ No newline at end of file diff --git a/version.py b/version.py deleted file mode 100644 index 39542079..00000000 --- a/version.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import subprocess -from sys import stdout - -try: - with open(os.devnull, 'w') as devnull: - v = subprocess.check_output(['git', 'describe', '--tags', '--dirty', '--abbrev'], stderr=stdout).decode().strip() - if '-' in v: - bv = v[:v.index('-')] - bv = bv[:bv.rindex('.') + 1] + str(int(bv[bv.rindex('.') + 1:]) + 1) - sempre = 'dirty' if v.endswith('-dirty') else 'commit' - pippre = 'alpha' if v.endswith('-dirty') else 'pre' - build = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode().strip() - number_since = subprocess.check_output( - ['git', 'rev-list', v[:v.index('-')] + '..HEAD', '--count']).decode().strip() - semver = bv + '-' + sempre + '+' + build - pipver = bv + pippre + number_since - winver = v[:v.index('-')] + '.' + number_since - else: - semver = v - pipver = v - winver = v + '.0' - - with open('version', 'w') as f: - print('Semantic version is ' + semver) - f.write(semver) - with open('pip_version', 'w') as f: - print('PIP version is ' + pipver) - f.write(pipver) - with open('win_version', 'w') as f: - print('Windows version is ' + winver) - f.write(winver) -except Exception as e: - print('Error calling git') - print(e) diff --git a/win_version b/win_version deleted file mode 100644 index 2770e01e..00000000 --- a/win_version +++ /dev/null @@ -1 +0,0 @@ -3.5.4.0 \ No newline at end of file