From 957862785a8dcd15d0e70bff1423752ff000d26e Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:44:57 -0400 Subject: [PATCH 01/15] Add pre-commit and disable most warnings --- .pre-commit-config.yaml | 9 +++++++++ .pylintrc | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..aa2f3d73 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: local + hooks: + - id: pylint + name: pylint + entry: python -m pylint + language: system + types: [python] + args: [--rcfile=.pylintrc] diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..9ac1e731 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,10 @@ +[MASTER] + +max-line-length = 120 + +disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120, E1123, C0209, R1710, W0621, C0121, + W0614, W0401, W1202, C0117, W0718, R0205, R0402, R0914, R1725, R1735, C0411, W0237, W0702, W0223, W0613, + W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, + E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, + W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, + W0101, C0302, E0110, W0603, R1701, W0106, R1721 \ No newline at end of file From 141f50b8eaa395fb6be6cfae513e8656ba2a7455 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:56:20 -0400 Subject: [PATCH 02/15] Fix whitespace --- .github/workflows/codeql.yml | 6 +-- .github/workflows/main.yml | 12 ++--- .pre-commit-config.yaml | 13 +++++ .pylintrc | 4 +- pip_version | 2 +- pros/cli/click_classes.py | 2 +- pros/cli/common.py | 2 +- pros/cli/conductor.py | 5 +- pros/cli/misc_commands.py | 4 +- pros/cli/terminal.py | 4 +- pros/cli/upload.py | 14 +++--- pros/conductor/conductor.py | 4 +- pros/conductor/depots.md | 8 +-- pros/conductor/project/__init__.py | 2 +- pros/ga/analytics.py | 10 ++-- pros/serial/ports/exceptions.py | 2 - pros/serial/ports/v5_wireless_port.py | 72 +++++++++++++-------------- version | 2 +- win_version | 2 +- 19 files changed, 90 insertions(+), 80 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a903ec32..3ea5f6b1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -48,11 +48,11 @@ jobs: # 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 @@ -61,7 +61,7 @@ jobs: # ℹī¸ 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. + # 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: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d597153..b956f490 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,24 +31,24 @@ jobs: - 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' @@ -59,7 +59,7 @@ jobs: name: pros-cli-wheel-${{needs.update_build_number.outputs.output1}} path: dist/* if: matrix.os == 'ubuntu-latest' - + - name: Run Pyinstaller run: | python3 version.py @@ -78,7 +78,7 @@ jobs: 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: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa2f3d73..c22efffd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,17 @@ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: mixed-line-ending + args: [--fix=lf] + - id: end-of-file-fixer + - id: check-yaml + - id: check-vcs-permalinks + - id: check-merge-conflict + - id: check-case-conflict + - id: check-ast + - id: trailing-whitespace + - id: requirements-txt-fixer - repo: local hooks: - id: pylint diff --git a/.pylintrc b/.pylintrc index 9ac1e731..16e8f317 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,7 +4,7 @@ max-line-length = 120 disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120, E1123, C0209, R1710, W0621, C0121, W0614, W0401, W1202, C0117, W0718, R0205, R0402, R0914, R1725, R1735, C0411, W0237, W0702, W0223, W0613, - W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, + W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, - W0101, C0302, E0110, W0603, R1701, W0106, R1721 \ No newline at end of file + W0101, C0302, E0110, W0603, R1701, W0106, R1721 diff --git a/pip_version b/pip_version index a423d421..4d9d11cf 100644 --- a/pip_version +++ b/pip_version @@ -1 +1 @@ -3.4.2 \ No newline at end of file +3.4.2 diff --git a/pros/cli/click_classes.py b/pros/cli/click_classes.py index cb5a82c0..8272f488 100644 --- a/pros/cli/click_classes.py +++ b/pros/cli/click_classes.py @@ -162,4 +162,4 @@ def invoke(self, *args, **kwargs): 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 + raise e diff --git a/pros/cli/common.py b/pros/cli/common.py index e666877d..b62c2d5b 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -139,7 +139,7 @@ def callback(ctx: click.Context, param: click.Parameter, value: bool): if value: echo("Not sending analytics for this command.\n") analytics.useAnalytics = False - pass + 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__ diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 79e098f1..3b4c8257 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -222,7 +222,7 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, 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) + + 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: @@ -311,7 +311,7 @@ def info_project(project: c.Project, ls_upgrades): Visit https://pros.cs.purdue.edu/v5/cli/conductor.html to learn more """ - analytics.send("info-project") + analytics.send("info-project") from pros.conductor.project import ProjectReport report = ProjectReport(project) _conductor = c.Conductor() @@ -366,4 +366,3 @@ def query_depots(url: bool): _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") - \ No newline at end of file diff --git a/pros/cli/misc_commands.py b/pros/cli/misc_commands.py index 8566456a..d212a2fc 100644 --- a/pros/cli/misc_commands.py +++ b/pros/cli/misc_commands.py @@ -19,9 +19,9 @@ def upgrade(force_check, no_install): """ 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() diff --git a/pros/cli/terminal.py b/pros/cli/terminal.py index 2f05f2fe..a44b89d5 100644 --- a/pros/cli/terminal.py +++ b/pros/cli/terminal.py @@ -42,7 +42,7 @@ def terminal(port: str, backend: str, **kwargs): 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 @@ -91,7 +91,7 @@ def __init__(self, file): self.log = open(file, 'a') def write(self, data): self.terminal.write(data) - self.log.write(data) + self.log.write(data) def flush(self): pass def end(self): diff --git a/pros/cli/upload.py b/pros/cli/upload.py index 545609a4..e0c74b9b 100644 --- a/pros/cli/upload.py +++ b/pros/cli/upload.py @@ -22,7 +22,7 @@ def upload_cli(): 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.', +@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('--name', 'remote_name', type=str, default=None, required=False, help='Remote program name.', @@ -37,9 +37,9 @@ def upload_cli(): 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', +@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', default=None, type=str, cls=PROSOption, group='V5 Options', +@click.option('--name', default=None, type=str, cls=PROSOption, group='V5 Options', help='Change the name of the program.') @default_options @@ -119,12 +119,12 @@ def upload(path: Optional[str], project: Optional[c.Project], port: str, **kwarg 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, + '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: diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index b8e50416..1080371d 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -177,7 +177,7 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: 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 - + return list(results) def resolve_template(self, identifier: Union[str, BaseTemplate], **kwargs) -> Optional[BaseTemplate]: @@ -340,6 +340,6 @@ def add_depot(self, name: str, url: str): 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 index 33a92336..f4efcf3f 100644 --- a/pros/conductor/depots.md +++ b/pros/conductor/depots.md @@ -13,7 +13,7 @@ $ pros conduct add-depot test "https://pros.cs.purdue.edu/v5/_static/beta/testin `pros conduct remove-depot ` Example: -```bash +```bash $ pros conduct remove-depot test > Removed depot test ``` @@ -28,11 +28,11 @@ 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 @@ -41,5 +41,5 @@ $ pros conduct query-depots > kernel-beta-mainline > pros-mainline > test -> +> ``` diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py index 76e4d192..486610ca 100644 --- a/pros/conductor/project/__init__.py +++ b/pros/conductor/project/__init__.py @@ -293,7 +293,7 @@ def libscanbuild_capture(args: argparse.Namespace) -> Tuple[int, Iterable[Compil 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: + if not suppress_output: pipe.close() sys.exit() if not suppress_output: diff --git a/pros/ga/analytics.py b/pros/ga/analytics.py index 6f786105..7a9c76af 100644 --- a/pros/ga/analytics.py +++ b/pros/ga/analytics.py @@ -53,9 +53,9 @@ def send(self,action): 'ni': 0 } - session = FuturesSession() + session = FuturesSession() - #Send payload to GA servers + #Send payload to GA servers future = session.post(url=url, data=payload, headers={'User-Agent': agent}, @@ -71,13 +71,13 @@ def set_use(self, value: bool): 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) @@ -92,4 +92,4 @@ def process_requests(self): return responses -analytics = Analytics() \ No newline at end of file +analytics = Analytics() diff --git a/pros/serial/ports/exceptions.py b/pros/serial/ports/exceptions.py index cd3f0bca..1a869f38 100644 --- a/pros/serial/ports/exceptions.py +++ b/pros/serial/ports/exceptions.py @@ -26,5 +26,3 @@ def __str__(self): 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/v5_wireless_port.py b/pros/serial/ports/v5_wireless_port.py index 80d4717d..dc25c259 100644 --- a/pros/serial/ports/v5_wireless_port.py +++ b/pros/serial/ports/v5_wireless_port.py @@ -1,36 +1,36 @@ -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 +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/version b/version index a423d421..4d9d11cf 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.4.2 \ No newline at end of file +3.4.2 diff --git a/win_version b/win_version index 0ccc3dcd..e44b972e 100644 --- a/win_version +++ b/win_version @@ -1 +1 @@ -3.4.2.0 \ No newline at end of file +3.4.2.0 From 63b2dbed7f177eb0ac1d2b949a7ce1d2e43ad2be Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:56:43 -0400 Subject: [PATCH 03/15] Sort alphabetically --- requirements.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0cbde8f3..6d3b9813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ +cachetools click>=6,<7 +cobs +colorama +jsonpickle +observable +pyinstaller +pypng==0.0.20 pyserial -cachetools +pyzmq requests requests-futures -tabulate -jsonpickle -semantic_version -colorama -pyzmq -cobs scan-build==2.0.13 +semantic_version sentry-sdk -observable -pypng==0.0.20 -pyinstaller \ No newline at end of file +tabulate From f015da39b003911694e979b333b33d50d5d5ea04 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:51:03 -0400 Subject: [PATCH 04/15] Fixed some errors --- .pylintrc | 5 ++--- pros/common/utils.py | 2 +- pros/conductor/project/ProjectTransaction.py | 1 - pros/conductor/project/__init__.py | 2 +- pros/ga/analytics.py | 2 +- pros/serial/devices/vex/crc.py | 2 +- pros/serial/devices/vex/v5_device.py | 5 ++--- pros/serial/ports/__init__.py | 2 +- 8 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.pylintrc b/.pylintrc index 16e8f317..5a21d69f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,10 +1,9 @@ [MASTER] max-line-length = 120 - -disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120, E1123, C0209, R1710, W0621, C0121, +disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120, E1123, C0209, R1710, W0621, C0121, W0614, W0401, W1202, C0117, W0718, R0205, R0402, R0914, R1725, R1735, C0411, W0237, W0702, W0223, W0613, W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, - W0101, C0302, E0110, W0603, R1701, W0106, R1721 + W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, diff --git a/pros/common/utils.py b/pros/common/utils.py index 294da89f..d74d9a2d 100644 --- a/pros/common/utils.py +++ b/pros/common/utils.py @@ -35,7 +35,7 @@ def get_version(): 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()): + for _, entry_point in iter(scripts.items()): if entry_point.module_name == module: ver = dist.version if ver is not None: diff --git a/pros/conductor/project/ProjectTransaction.py b/pros/conductor/project/ProjectTransaction.py index 14034d42..edae1330 100644 --- a/pros/conductor/project/ProjectTransaction.py +++ b/pros/conductor/project/ProjectTransaction.py @@ -36,7 +36,6 @@ def execute(self, conductor: c.Conductor, project: c.Project): 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)) diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py index 486610ca..a0777595 100644 --- a/pros/conductor/project/__init__.py +++ b/pros/conductor/project/__init__.py @@ -411,7 +411,7 @@ def find_project(path: str, recurse_times: int = 10): if os.path.isfile(path): path = os.path.dirname(path) if os.path.isdir(path): - for n in range(recurse_times): + for _ 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'] diff --git a/pros/ga/analytics.py b/pros/ga/analytics.py index 7a9c76af..247e6b31 100644 --- a/pros/ga/analytics.py +++ b/pros/ga/analytics.py @@ -62,7 +62,7 @@ def send(self,action): timeout=5.0) self.pendingRequests.append(future) - except Exception as e: + except Exception: from pros.cli.common import logger logger(__name__).warning("Unable to send analytics. Do you have a stable internet connection?", extra={'sentry': False}) diff --git a/pros/serial/devices/vex/crc.py b/pros/serial/devices/vex/crc.py index f53bee5d..2e4270d7 100644 --- a/pros/serial/devices/vex/crc.py +++ b/pros/serial/devices/vex/crc.py @@ -9,7 +9,7 @@ def __init__(self, size: int, polynomial: int): for i in range(256): crc_accumulator = i << (self._size - 8) - for j in range(8): + for _ in range(8): if crc_accumulator & (1 << (self._size - 1)): crc_accumulator = (crc_accumulator << 1) ^ self._polynomial else: diff --git a/pros/serial/devices/vex/v5_device.py b/pros/serial/devices/vex/v5_device.py index a19d0777..124897e0 100644 --- a/pros/serial/devices/vex/v5_device.py +++ b/pros/serial/devices/vex/v5_device.py @@ -268,7 +268,6 @@ def upload_project(self, project: Project, **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()), @@ -612,7 +611,7 @@ def read_ini(self, remote_name: str) -> Optional[ConfigParser]: rx_io.seek(0, 0) config.read_string(rx_io.read().decode('ascii')) return config - except VEXCommError as e: + except VEXCommError: return None @retries @@ -918,7 +917,7 @@ def kv_write(self, kv: str, payload: Union[Iterable, bytes, bytearray, 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) + self._txrx_ext_packet(0x2f, tx_payload, 1, check_length=False, check_ack=True) logger(__name__).debug('Completed ext 0x2f command') return payload diff --git a/pros/serial/ports/__init__.py b/pros/serial/ports/__init__.py index be344a79..4850b2b9 100644 --- a/pros/serial/ports/__init__.py +++ b/pros/serial/ports/__init__.py @@ -1,7 +1,7 @@ from functools import lru_cache from pros.common import logger -from serial.tools import list_ports as list_ports +from serial.tools import list_ports from .base_port import BasePort, PortConnectionException, PortException from .direct_port import DirectPort From 491f4e68531b9c5308e801fcea83e6dac8db99fc Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:42:16 -0400 Subject: [PATCH 05/15] Add pre-commit and pylint --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 6d3b9813..ae307540 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,9 @@ cobs colorama jsonpickle observable +pre-commit pyinstaller +pylint pypng==0.0.20 pyserial pyzmq From a94380595044e0f054f8510506025fcd2ae2ca7e Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:52:54 -0400 Subject: [PATCH 06/15] Add pylint to github actions --- .github/workflows/main.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d597153..74eba702 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,27 @@ on: pull_request: jobs: + pylint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install -r requirements.txt + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') --rcfile=$(git ls-files '.pylintrc') + update_build_number: runs-on: ubuntu-latest outputs: From 9346d25e484351aa258f274ba5a6e7b0f320fc17 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:57:22 -0400 Subject: [PATCH 07/15] Add rcfile --- .pylintrc | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..5a21d69f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,9 @@ +[MASTER] + +max-line-length = 120 +disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120, E1123, C0209, R1710, W0621, C0121, + W0614, W0401, W1202, C0117, W0718, R0205, R0402, R0914, R1725, R1735, C0411, W0237, W0702, W0223, W0613, + W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, + E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, + W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, + W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, From f251a16c79f84644cd7958c99b6423a31ae81923 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:02:14 -0400 Subject: [PATCH 08/15] Disable cyclic import --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 5a21d69f..4514f984 100644 --- a/.pylintrc +++ b/.pylintrc @@ -6,4 +6,4 @@ disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120 W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, - W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, + W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, R0401 From 682a4917e194fc0d7f760b8629d2efaf7bd587b0 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:17:20 -0500 Subject: [PATCH 09/15] Add pre-commit and pylint --- requirements.txt | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 48d35bc3..9bc7673c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ -click>=8 -rich-click -pyserial -pyzmq -requests -requests-futures -scan-build==2.0.13 -semantic_version -sentry-sdk -observable -pypng==0.0.20 -pyinstaller +click>=8 +observable +pre-commit +pyinstaller +pylint +pypng==0.0.20 +pyserial +pyzmq +requests +requests-futures +rich-click +scan-build==2.0.13 +semantic_version +sentry-sdk From 5eaccfb792fc663353f54e81b0e08efd085ba823 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:18:28 -0500 Subject: [PATCH 10/15] Fix whitespace --- pip_version | 2 +- pros/conductor/conductor.py | 712 ++++++++++++++++++------------------ version | 2 +- win_version | 2 +- 4 files changed, 359 insertions(+), 359 deletions(-) diff --git a/pip_version b/pip_version index 8a0feb98..6cb9d3dd 100644 --- a/pip_version +++ b/pip_version @@ -1 +1 @@ -3.4.3 \ No newline at end of file +3.4.3 diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index c2766f74..86ef05cd 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -1,356 +1,356 @@ -import os.path -import shutil -from enum import Enum -from pathlib import Path -from typing import * - -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' -""" - -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.default_libraries: Dict[str, List[str]] = None - self.early_access_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.default_libraries is None: - self.default_libraries = { - 'v5': ['okapilib'], - 'cortex': [] - } - needs_saving = True - if self.early_access_libraries is None or len(self.early_access_libraries['v5']) != 2: - self.early_access_libraries = { - 'v5': ['liblvgl', 'okapilib'], - 'cortex': [] - } - needs_saving = True - if 'v5' not in self.default_libraries: - self.default_libraries['v5'] = [] - needs_saving = True - if 'cortex' not in self.default_libraries: - self.default_libraries['cortex'] = [] - needs_saving = True - if 'v5' not in self.early_access_libraries: - self.early_access_libraries['v5'] = [] - needs_saving = True - if 'cortex' not in self.early_access_libraries: - self.early_access_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: - self.use_early_access = kwargs.get('early_access', False) - if isinstance(identifier, str): - query = BaseTemplate.create_query(name=identifier, **kwargs) - else: - query = identifier - if allow_offline: - if self.use_early_access: - offline_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.early_access_local_templates)) - else: - offline_results = list(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 self.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 (kernel_version.split('.')[0] == '3' and not self.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}')) - - # 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')) - 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')) - elif not self.use_early_access and template.version[0] == '3' and not self.warn_early_access: - confirm = ui.confirm(f'PROS 4 is now in early access. ' - f'Please use the --early-access flag if you would like to use it.\n' - f'Do you want to use PROS 4 instead?') - self.warn_early_access = True - if confirm: # use pros 4 - self.use_early_access = True - kwargs['version'] = '>=0' - self.save() - # Recall the function with early access enabled - return self.apply_template(project, identifier, **kwargs) - - self.save() - 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}') - 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: - self.use_early_access = kwargs.get('early_access', False) - if kwargs["version_source"]: # If true, then the user has not specified a version - if not self.use_early_access and self.warn_early_access: - ui.echo(f"PROS 4 is now in early access. " - f"If you would like to use it, use the --early-access flag.") - elif self.use_early_access: - ui.echo(f'Early access is enabled. Using PROS 4.') - elif self.use_early_access: - ui.echo(f'Early access is enabled.') - - if Path(path).exists() and Path(path).samefile(os.path.expanduser('~')): - raise dont_send(ValueError('Will not create a project in user home directory')) - for char in str(Path(path)): - if char in ['?', '<', '>', '*', '|', '^', '#', '%', '&', '$', '+', '!', '`', '\'', '=', - '@', '\'', '{', '}', '[', ']', '(', ')', '~'] or ord(char) > 127: - raise dont_send(ValueError(f'Invalid character found in directory name: \'{char}\'')) - - proj = Project(path=path, create=True) - 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: - libraries = self.early_access_libraries if self.use_early_access else self.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()] +import os.path +import shutil +from enum import Enum +from pathlib import Path +from typing import * + +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' +""" + +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.default_libraries: Dict[str, List[str]] = None + self.early_access_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.default_libraries is None: + self.default_libraries = { + 'v5': ['okapilib'], + 'cortex': [] + } + needs_saving = True + if self.early_access_libraries is None or len(self.early_access_libraries['v5']) != 2: + self.early_access_libraries = { + 'v5': ['liblvgl', 'okapilib'], + 'cortex': [] + } + needs_saving = True + if 'v5' not in self.default_libraries: + self.default_libraries['v5'] = [] + needs_saving = True + if 'cortex' not in self.default_libraries: + self.default_libraries['cortex'] = [] + needs_saving = True + if 'v5' not in self.early_access_libraries: + self.early_access_libraries['v5'] = [] + needs_saving = True + if 'cortex' not in self.early_access_libraries: + self.early_access_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: + self.use_early_access = kwargs.get('early_access', False) + if isinstance(identifier, str): + query = BaseTemplate.create_query(name=identifier, **kwargs) + else: + query = identifier + if allow_offline: + if self.use_early_access: + offline_results = list(filter(lambda t: t.satisfies(query, kernel_version=kernel_version), self.early_access_local_templates)) + else: + offline_results = list(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 self.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 (kernel_version.split('.')[0] == '3' and not self.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}')) + + # 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')) + 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')) + elif not self.use_early_access and template.version[0] == '3' and not self.warn_early_access: + confirm = ui.confirm(f'PROS 4 is now in early access. ' + f'Please use the --early-access flag if you would like to use it.\n' + f'Do you want to use PROS 4 instead?') + self.warn_early_access = True + if confirm: # use pros 4 + self.use_early_access = True + kwargs['version'] = '>=0' + self.save() + # Recall the function with early access enabled + return self.apply_template(project, identifier, **kwargs) + + self.save() + 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}') + 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: + self.use_early_access = kwargs.get('early_access', False) + if kwargs["version_source"]: # If true, then the user has not specified a version + if not self.use_early_access and self.warn_early_access: + ui.echo(f"PROS 4 is now in early access. " + f"If you would like to use it, use the --early-access flag.") + elif self.use_early_access: + ui.echo(f'Early access is enabled. Using PROS 4.') + elif self.use_early_access: + ui.echo(f'Early access is enabled.') + + if Path(path).exists() and Path(path).samefile(os.path.expanduser('~')): + raise dont_send(ValueError('Will not create a project in user home directory')) + for char in str(Path(path)): + if char in ['?', '<', '>', '*', '|', '^', '#', '%', '&', '$', '+', '!', '`', '\'', '=', + '@', '\'', '{', '}', '[', ']', '(', ')', '~'] or ord(char) > 127: + raise dont_send(ValueError(f'Invalid character found in directory name: \'{char}\'')) + + proj = Project(path=path, create=True) + 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: + libraries = self.early_access_libraries if self.use_early_access else self.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/version b/version index 8a0feb98..6cb9d3dd 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.4.3 \ No newline at end of file +3.4.3 diff --git a/win_version b/win_version index 20cef1f4..03f59952 100644 --- a/win_version +++ b/win_version @@ -1 +1 @@ -3.4.3.0 \ No newline at end of file +3.4.3.0 From 29570d16dc72101bf5d4ce8b9b83c907aa326041 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:34:16 -0500 Subject: [PATCH 11/15] Readd removed requirements --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requirements.txt b/requirements.txt index 9bc7673c..7b20814b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ +cachetools click>=8 +cobs +colorama +jsonpickle observable pre-commit pyinstaller @@ -12,3 +16,4 @@ rich-click scan-build==2.0.13 semantic_version sentry-sdk +tabulate From c57fd3c72ea81a246b43010426c1545d590d7061 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:15:49 -0500 Subject: [PATCH 12/15] Add pre-commit action --- .github/workflows/pre-commit.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..65080b90 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + +env: + SKIP: pylint + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/setup-python@v5.0.0 + - uses: pre-commit/action@v3.0.0 From 975e51f4c994ab0d0821321682905870e766561e Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:16:17 -0500 Subject: [PATCH 13/15] Move pylint action to separate file --- .github/workflows/main.yml | 21 --------------------- .github/workflows/pylint.yml | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/pylint.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d004c816..76145e6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,27 +5,6 @@ on: pull_request: jobs: - pylint: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.10"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - pip install -r requirements.txt - - name: Analysing the code with pylint - run: | - pylint $(git ls-files '*.py') --rcfile=$(git ls-files '.pylintrc') - update_build_number: runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..5af7078b --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,26 @@ +name: Pylint + +on: + pull_request: + push: + +jobs: + Pylint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v4.1.1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') --rcfile=$(git ls-files '.pylintrc') From 0215148c81edea6b91c5ff763a54b90ce48be478 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:18:28 -0500 Subject: [PATCH 14/15] Remove pre-commit action --- .github/workflows/pre-commit.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 65080b90..00000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - -env: - SKIP: pylint - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-python@v5.0.0 - - uses: pre-commit/action@v3.0.0 From ec264b84f56ffc0c05418e0b866aece932d09551 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:26:45 -0500 Subject: [PATCH 15/15] Disable whitespace and max builtin --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 117c463f..8d811779 100644 --- a/.pylintrc +++ b/.pylintrc @@ -6,4 +6,4 @@ disable = C0114, C0115, C0116, R0903, C0415, R1705, R0913, W1203, R1729, E1120 W0108, R0912, R0911, W0511, E1136, R0902, W0611, C0412, C0103, C0301, R1732, R0915, W1514, R1718, W1510, E0602, W1309, C0325, E1101, R1714, R0916, W0719, R1734, E1133, W1201, W0107, W3101, W0640, C0201, W1113, W0246, W0622, W0221, E1111, R1720, W0221, R1723, E0102, W0201, E0203, E0401, W0602, W0212, W0707, R0904, - W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, R0401, W0612, W1406 + W0101, C0302, E0110, W0603, R1701, W0106, R1721, W0601, R0401, W0612, W1406, C0303, R1731