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 76d5fcfd..00000000 Binary files a/pros.icns and /dev/null differ 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