From 10b38af32c9c2ec8fafe76868d156bfa22989ec7 Mon Sep 17 00:00:00 2001 From: noam987 <50681033+noam987@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:51:34 -0500 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=90=9B=20Better=20error=20message=20f?= =?UTF-8?q?or=20File=20not=20found=20on=20fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pros/cli/conductor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 06b26e86..cdeaa9c2 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -64,6 +64,9 @@ def fetch(query: c.BaseTemplate): 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: From 5efc1e3a06e68e06093eef4a7d6027d3ddfc2be3 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:45:47 -0500 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=90=9B=20Fix=20rich=20click=20for=20m?= =?UTF-8?q?ake=20compile=20error=20(#327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix rich click for make compile error * Also fix in build-compile-commands --- pros/cli/build.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pros/cli/build.py b/pros/cli/build.py index bd9fdcb8..9ed2a742 100644 --- a/pros/cli/build.py +++ b/pros/cli/build.py @@ -1,3 +1,5 @@ +import ctypes +import sys from typing import * import click @@ -24,6 +26,10 @@ def make(project: c.Project, build_args): 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 @@ -71,6 +77,10 @@ def build_compile_commands(project: c.Project, suppress_output: bool, compile_co 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 From 18fb0904badd3ceb61bbe773b07e2baadedeb39e Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Mon, 12 Feb 2024 12:14:39 -0500 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8=20Add=20command=20to=20reset=20co?= =?UTF-8?q?nductor.pros=20(#320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update version numbers * 🐛 Fix default template selection #318 * Update version numbers * Add command to reset conductor.pros * Update pros/cli/conductor.py Co-authored-by: BennyBot <48661356+BennyBot@users.noreply.github.com> --------- Co-authored-by: ayushuk Co-authored-by: Ayush Shukla <71904196+ayushuk@users.noreply.github.com> Co-authored-by: BennyBot <48661356+BennyBot@users.noreply.github.com> --- pros/cli/conductor.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index cdeaa9c2..73904ccd 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -371,4 +371,27 @@ 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") + + +@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") \ No newline at end of file From 3998579e9161a42552b8dfaeb1b4b62f7cca51c1 Mon Sep 17 00:00:00 2001 From: THERocky <101498190+Rocky14683@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:44:36 -0500 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20Port=20selection=20(#326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Found a typo * add feature on selecting brain based on brains' unique ID --- pros/cli/common.py | 10 ++++++---- pros/cli/upload.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pros/cli/common.py b/pros/cli/common.py index 71c589c3..d658c7eb 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -253,11 +253,13 @@ def resolve_v5_port(port: Optional[str], type: str, quiet: bool = False) -> Tupl return None, False if len(ports) > 1: if not quiet: - port = click.prompt('Multiple {} ports were found. Please choose one: [{}]' - .format('v5', '|'.join([p.device for p in ports])), - default=ports[0].device, + 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.device for p in ports])) + 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 diff --git a/pros/cli/upload.py b/pros/cli/upload.py index 545609a4..5a50f3bd 100644 --- a/pros/cli/upload.py +++ b/pros/cli/upload.py @@ -192,7 +192,7 @@ def __str__(self): 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')) + 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')) From 31ae7445373469c5682b3091b89598b6268d0dad Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:12:01 -0500 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=90=9B=20Fix=20aliases=20in=20pros=20?= =?UTF-8?q?v5=20(#333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Explicitly name sv and rv * Add set_variable and read_variable --- pros/cli/v5_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pros/cli/v5_utils.py b/pros/cli/v5_utils.py index 7c0809bd..a6fe0eec 100644 --- a/pros/cli/v5_utils.py +++ b/pros/cli/v5_utils.py @@ -291,7 +291,7 @@ def capture(file_name: str, port: str, force: bool = False): print(f'Saved screen capture to {file_name}') -@v5.command(aliases=['sv', 'set'], short_help='Set a kernel variable on a connected V5 device') +@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) @@ -308,7 +308,7 @@ def set_variable(variable, value, port): actual_value = device.kv_write(variable, value).decode() print(f'Value of \'{variable}\' set to : {actual_value}') -@v5.command(aliases=['rv', 'get'], short_help='Read a kernel variable from a connected V5 device') +@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 From a73b3e09271178b4f52fe2c398a305dac456d3a4 Mon Sep 17 00:00:00 2001 From: Ayush Shukla <71904196+ayushuk@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:13:12 -0500 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=9A=B8=20Add=20single=20letter=20quer?= =?UTF-8?q?y-templates=20alias=20(#335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pros/cli/conductor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index 73904ccd..e540e574 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -251,7 +251,7 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, @conductor.command('query-templates', - aliases=['search-templates', 'ls-templates', 'lstemplates', 'querytemplates', 'searchtemplates'], + 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') @@ -394,4 +394,4 @@ def reset(force: bool): os.remove(file) ui.echo("Conductor was reset") - \ No newline at end of file + From 08c0874fbaf54218873ca84d88ae334eea8ef7e0 Mon Sep 17 00:00:00 2001 From: Mayank Patibandla <34776435+mayankpatibandla@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:16:59 -0500 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=9A=B8=20Disable=20sentry=20confirmat?= =?UTF-8?q?ion=20promp=20(#334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pros/cli/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pros/cli/common.py b/pros/cli/common.py index d658c7eb..eb546db0 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -124,7 +124,7 @@ def callback(ctx: click.Context, param: click.Parameter, value: bool): add_tag('no-sentry',value) if value: pros.common.sentry.disable_prompt() - decorator = click.option('--no-sentry', expose_value=False, is_flag=True, default=False, is_eager=True, + 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 From a335b80c7d30099f400de10c52c67a4ef88e3bdb Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Sun, 18 Feb 2024 11:21:01 -0600 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=9A=B8=20Filename=20validator=20for?= =?UTF-8?q?=20new=20projects=20(#298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use regex * better path checker * check for emojis in path * change err message * fix validation fail oops --- pros/conductor/conductor.py | 56 +++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index e3547134..51eee752 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -1,8 +1,11 @@ +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 @@ -27,6 +30,50 @@ class ReleaseChannel(Enum): 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) @@ -314,13 +361,12 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro elif self.use_early_access: ui.echo(f'Early access is enabled.') + 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')) - 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'] From 30dfe919316ef91ac3656c291fe6a47f93472079 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Sun, 18 Feb 2024 11:59:27 -0600 Subject: [PATCH 9/9] =?UTF-8?q?=E2=9C=A8=20Make=20early=20access=20project?= =?UTF-8?q?=20scope=20(#330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * project scope early access * add global default flag * Remove extra resolve_templates call * use project's ea setting in query templates * Add back early access project parameter --------- Co-authored-by: ayushuk Co-authored-by: Ayush Shukla <71904196+ayushuk@users.noreply.github.com> --- pros/cli/common.py | 5 +++- pros/cli/conductor.py | 19 ++++++-------- pros/cli/main.py | 21 ++++++++++++++- pros/conductor/conductor.py | 42 ++++++++++++++++++++---------- pros/conductor/project/__init__.py | 3 ++- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/pros/cli/common.py b/pros/cli/common.py index eb546db0..6c12fa06 100644 --- a/pros/cli/common.py +++ b/pros/cli/common.py @@ -191,10 +191,13 @@ def callback(ctx: click.Context, param: click.Parameter, value: str): if project_path is None: if allow_none: return None - else: + 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]): diff --git a/pros/cli/conductor.py b/pros/cli/conductor.py index e540e574..00a0ecb5 100644 --- a/pros/cli/conductor.py +++ b/pros/cli/conductor.py @@ -94,7 +94,7 @@ def fetch(query: c.BaseTemplate): 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/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, +@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) @@ -145,7 +145,7 @@ def install(ctx: click.Context, **kwargs): 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/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, +@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) @@ -207,7 +207,7 @@ def uninstall_template(project: c.Project, query: c.BaseTemplate, remove_user: b 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/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, +@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 @@ -261,12 +261,13 @@ def new_project(ctx: click.Context, path: str, target: str, version: str, 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/--disable-early-access', '--early/--disable-early', '-ea/-dea', 'early_access', '--beta/--disable-beta', default=None, +@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, query: c.BaseTemplate, allow_offline: bool, allow_online: bool, force_refresh: bool, +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 @@ -276,12 +277,10 @@ def query_templates(ctx, query: c.BaseTemplate, allow_offline: bool, allow_onlin 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) - if early_access: - templates += c.Conductor().resolve_templates(query, allow_offline=allow_offline, allow_online=allow_online, - force_refresh=force_refresh, early_access=False) - render_templates = {} for template in templates: key = (template.identifier, template.origin) @@ -372,7 +371,6 @@ def query_depots(url: bool): 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 @@ -394,4 +392,3 @@ def reset(force: bool): os.remove(file) ui.echo("Conductor was reset") - diff --git a/pros/cli/main.py b/pros/cli/main.py index 679d9897..8e4d6725 100644 --- a/pros/cli/main.py +++ b/pros/cli/main.py @@ -27,6 +27,7 @@ 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 @@ -99,7 +100,23 @@ def use_analytics(ctx: click.Context, param, value): ctx.exit(0) ctx.ensure_object(dict) analytics.set_use(touse) - ui.echo('Analytics set to : {}'.format(analytics.useAnalytics)) + 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) @@ -112,6 +129,8 @@ def use_analytics(ctx: click.Context, param, value): 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) diff --git a/pros/conductor/conductor.py b/pros/conductor/conductor.py index 51eee752..fb40d7e1 100644 --- a/pros/conductor/conductor.py +++ b/pros/conductor/conductor.py @@ -188,7 +188,9 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: 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) + 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: @@ -196,7 +198,7 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: if allow_offline: offline_results = list() - if self.use_early_access: + 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)) @@ -208,7 +210,7 @@ def resolve_templates(self, identifier: Union[str, BaseTemplate], allow_online: 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): + 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)) @@ -219,8 +221,8 @@ 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 - - if len(results) == 0 and not self.use_early_access: + + if len(results) == 0 and not use_early_access: raise dont_send( InvalidTemplateException(f'{identifier.name} does not support kernel version {kernel_version}')) @@ -298,15 +300,16 @@ def apply_template(self, project: Project, identifier: Union[str, BaseTemplate], 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: + elif not project.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 + project.use_early_access = True + project.save() kwargs['version'] = '>=0' - self.save() + kwargs['early_access'] = True # Recall the function with early access enabled return self.apply_template(project, identifier, **kwargs) @@ -351,14 +354,25 @@ def remove_template(project: Project, identifier: Union[str, BaseTemplate], remo 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) + use_early_access = kwargs.get('early_access', False) + else: + use_early_access = self.use_early_access + kwargs["early_access"] = use_early_access 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: + if not 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: + elif not use_early_access 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_early_access = True + kwargs['early_access'] = True + elif use_early_access: ui.echo(f'Early access is enabled. Using PROS 4.') - elif self.use_early_access: + elif use_early_access: ui.echo(f'Early access is enabled.') if not is_pathname_valid(str(Path(path).absolute())): @@ -367,7 +381,7 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro 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) + 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(): @@ -381,7 +395,7 @@ def new_project(self, path: str, no_default_libs: bool = False, **kwargs) -> Pro proj.save() if not no_default_libs: - libraries = self.early_access_libraries if self.use_early_access and (kwargs.get("version", ">").startswith("4") or kwargs.get("version", ">").startswith(">")) else self.default_libraries + libraries = self.early_access_libraries if proj.use_early_access and (kwargs.get("version", ">").startswith("4") or kwargs.get("version", ">").startswith(">")) else self.default_libraries for library in libraries[proj.target]: try: # remove kernel version so that latest template satisfying query is correctly selected diff --git a/pros/conductor/project/__init__.py b/pros/conductor/project/__init__.py index b30733b5..c71fcd1f 100644 --- a/pros/conductor/project/__init__.py +++ b/pros/conductor/project/__init__.py @@ -16,7 +16,7 @@ class Project(Config): - def __init__(self, path: str = '.', create: bool = False, raise_on_error: bool = True, defaults: dict = None): + 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, @@ -38,6 +38,7 @@ def __init__(self, path: str = '.', create: bool = False, raise_on_error: bool = 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