diff --git a/.gitignore b/.gitignore index 715dfd9..caca6c8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ coverage.xml dist/ htmlcov/ venv/ +.venv/ version.py *.sublime-project *.sublime-workspace +*.code-workspace +.vscode/ diff --git a/README.md b/README.md index aec1ee7..a7d0d5e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ dependency resolution. It provides a habitat for you to work in. Features: -* [URI](#uri) based configuration resolution with inheritance. Makes it easy to define +* [URI](#URI) based configuration resolution with inheritance. Makes it easy to define generic settings, but override for child URIs. * Simple [configuration](#configuration) with json files. * Site configuration, code distributions are separate from URI configurations. All of @@ -95,8 +95,8 @@ end up launching the same application, See [Multiple app versions](#multiple-app ### User Prefs To support reusable alias shortcuts, hab has the ability to remember a URI and -reuse it for the next run. Anywhere you pass a uri to the cli, you can pass a -dash `-` instead. Hab will use the saved uri. +reuse it for the next run. Anywhere you pass a URI to the cli, you can pass a +dash `-` instead. Hab will use the saved URI. ```bash $ hab env - @@ -109,9 +109,11 @@ hab --prefs dump - ``` The site configuration may also configure a timeout that will require you to re-specify -the uri after a while. +the URI after a while. -To update the URI used when you pass `-`, pass `--save-prefs` after hab. You can +If you try to use `-` with an expired URI, Hab will notify you and prompt you to re-specify a URI. + +To update the URI manually, pass `--save-prefs` after hab. You can not use `-` when using this option. ```bash hab --save-prefs dump project_a/Seq001/S0010 @@ -173,7 +175,7 @@ $env:HAB_PATHS="c:\path\to\site_b.json;c:\path\to\site_a.json" `identifier1/identifier2/...` You specify a configuration using a simple URI of identifiers separated by a `/`. -Currently hab only supports absolute uri's. +Currently hab only supports absolute URI's. Examples: * projectDummy/Sc001/S0001.00 @@ -181,7 +183,7 @@ Examples: * projectDummy/Thug * default -If the provided uri has no configurations provided, the default configuration is used. +If the provided URI has no configurations provided, the default configuration is used. This also supports inheritance with some special rules, see [config inheritance](#config-inheritance) for more details. @@ -214,7 +216,7 @@ $ hab env projectDummy/Thug ``` The cli prompt is updated while inside a hab config is active. It is `[URI] [cwd]` -Where URI is the uri requested and cwd is the current working directory. +Where URI is the URI requested and cwd is the current working directory. ## Restoring resolved configuration diff --git a/hab/cli.py b/hab/cli.py index b1eb0e3..a786883 100644 --- a/hab/cli.py +++ b/hab/cli.py @@ -30,48 +30,80 @@ class UriArgument(click.Argument): When using a user pref, a message is written to the error stream to ensure the user can see what uri was resolved. It is written to the error stream so it doesn't interfere with capturing output to a file(json). + - If the timestamp on the user_prefs.json is lapse, the user will be prompted + to address the out of date URI by entering a new path or using the already + saved path. This also handles saving the provided uri to user prefs if enabled by `SharedSettings.enable_user_prefs_save`. This is only respected if an uri is provided. Ie if a frozen uri, json file or `-` are passed, prefs are not saved. + + Note: Using `err=True` so this output doesn't affect capturing of hab output from + cmds like `hab dump - --format json > output.json `. """ + def __uri_prompt(self, uri=None): + """Wrapper function of click.prompt. + Used to get a URI entry from the user""" + if uri: + response = click.prompt( + "Please enter a new URI...\n" + "Or press ENTER to reuse the expired URI:" + f" [{Fore.LIGHTBLUE_EX}{uri}{Fore.RESET}]", + default=uri, + show_default=False, + type=str, + err=True, + ) + else: + response = click.prompt( + "No URI exists.\nPlease Enter a URI", type=str, err=True + ) + return response + def type_cast_value(self, ctx, value): """Convert and validate the uri value. This override handles saving the uri to user prefs if enabled by the cli. """ - # User didn't specify a URI, return/raise an UsageError if value is None: result = click.UsageError("Missing argument 'URI'") if self.required: raise result return result - # User wants to use saved user prefs for the uri if value == "-": - value, reason = ctx.obj.resolver.user_prefs().uri_reason() - - if value is None: - # If there isn't a valid uri preference, raise a UsageError if its - # required, otherwise return the UsageError to the command so it - # can handle it - result = click.UsageError(f"Invalid 'URI' preference: {reason}") - if self.required: - raise result - return result + uri_check = ctx.obj.resolver.user_prefs().uri_check() + # This will indicate that no user_pref.json was saved + # and the user will be required to enter a uri path. + if uri_check.uri is None: + return self.__uri_prompt() + # Check if the saved user_prefs.json has an expire timestamp + elif uri_check.timedout: + logger.info( + f"{Fore.RED}Invalid 'URI' preference: {Fore.RESET}" + f"The saved URI {Fore.LIGHTBLUE_EX}{uri_check.uri}{Fore.RESET} " + f"has expired.", + ) + # The uri is expired so lets ask the user for a new uri + value = self.__uri_prompt(uri_check.uri) + if value: + # Saving a new user_prefs.json + ctx.obj.resolver.user_prefs().uri = value + click.echo("Saving user_prefs.json", err=True) + return value + else: + if self.required: + raise click.UsageError("A URI is required for Hab use.") + # user_pref.json is found and its saved uri will be used else: - # Indicate to the user that they are using a user pref, and make - # it easy to know what uri they are using when debugging the output. - # Note: Using `err=True` so this output doesn't affect capturing - # of hab output from cmds like `hab dump - --format json`. click.echo( - f'Using "{Fore.GREEN}{value}{Fore.RESET}" from user prefs.', + f"Using {Fore.LIGHTBLUE_EX}{uri_check.uri}{Fore.RESET} " + "from user_prefs.json", err=True, ) # Don't allow users to re-save the user prefs value when using # a user prefs value so they don't constantly reset the timeout. - return value - + return uri_check.uri # User passed a frozen hab string if re.match(r'^v\d+:', value): return decode_freeze(value) @@ -209,6 +241,7 @@ def write_script( _verbose_errors = False +# Establish CLI command group @click.group(context_settings=CONTEXT_SETTINGS) @click.version_option(__version__, prog_name="hab") @click.option( @@ -303,6 +336,7 @@ def _cli( logging.basicConfig(level=level) +# env command @_cli.command(cls=UriHelpClass) @click.argument("uri", cls=UriArgument) @click.option( @@ -317,6 +351,7 @@ def env(settings, uri, launch): settings.write_script(uri, create_launch=True, launch=launch) +# dump command @_cli.command(cls=UriHelpClass) # For specific report_types uri is not required. This is manually checked in # the code below where it raises `uri_error`. @@ -416,6 +451,8 @@ def echo_line(line): else: ret = settings.resolver.closest_config(uri) + # This is a seperate set of if/elif/else statements than from above. + # I became confused while reading so decided to add this reminder. if format_type == "freeze": ret = encode_freeze( ret.freeze(), version=settings.resolver.site.get("freeze_version") @@ -432,6 +469,7 @@ def echo_line(line): click.echo(ret) +# activate command @_cli.command(cls=UriHelpClass) @click.argument("uri", cls=UriArgument) @click.option( @@ -466,6 +504,7 @@ def activate(settings, uri, launch): settings.write_script(uri, launch=launch) +# launch command @_cli.command( context_settings=dict( ignore_unknown_options=True, diff --git a/hab/user_prefs.py b/hab/user_prefs.py index 37bf4ee..b7d11de 100644 --- a/hab/user_prefs.py +++ b/hab/user_prefs.py @@ -8,6 +8,26 @@ logger = logging.getLogger(__name__) +class UriObj: + def __init__(self, uri=None, timedout=False): + self._uri = uri + self._timedout = timedout + + def __str__(self): + if self.uri is None: + return '' + else: + return self.uri + + @property + def timedout(self): + return self._timedout + + @property + def uri(self): + return self._uri + + class UserPrefs(dict): """Stores/restores hab user preferences in a json file.""" @@ -113,31 +133,22 @@ def _fromisoformat(cls, value): iso_format = r"%Y-%m-%dT%H:%M:%S.%f" return datetime.datetime.strptime(value, iso_format) - def uri_reason(self): + def uri_check(self): """Returns the uri saved in preferences. It will only do that if enabled and uri_is_timedout allow it. Returns None otherwise. This will call load to ensure the preference file has been loaded. """ - reason = "User preferences are disabled." if self.enabled: # Ensure the preferences are loaded. self.load() - uri = self.get("uri") - reason = "No uri preference has been saved." if uri: - is_timedout = self.uri_is_timedout - # Only restore the uri if it hasn't expired and is enabled - if is_timedout: - reason = f"The saved uri {self['uri']} expired and needs re-saved." - else: - return self["uri"], "Uri was restored." - - return None, reason + return UriObj(uri, self.uri_is_timedout) + return UriObj() @property def uri(self): - return self.uri_reason()[0] + return self.uri_check().uri @uri.setter def uri(self, uri): @@ -147,7 +158,7 @@ def uri(self, uri): self["uri"] = uri self["uri_last_changed"] = datetime.datetime.today() self.save() - logger.debug(f'User prefs saved to "{self.filename}"') + logger.debug(f'User prefs saved to {self.filename}') @property def uri_last_changed(self): diff --git a/tests/test_user_prefs.py b/tests/test_user_prefs.py index 458967a..bd50b15 100644 --- a/tests/test_user_prefs.py +++ b/tests/test_user_prefs.py @@ -122,10 +122,10 @@ def test_uri(resolver, tmpdir, monkeypatch): prefs_d._enabled = True # Timeout has not expired resolver.site["prefs_uri_timeout"] = dict(hours=2) - assert prefs_d.uri == "app/aliased" + assert prefs_d.uri_check().timedout is False # Timeout has expired resolver.site["prefs_uri_timeout"] = dict(minutes=5) - assert prefs_d.uri is None + assert prefs_d.uri_check().timedout is True # Check that uri.setter is processed correctly prefs_e = user_prefs.UserPrefs(resolver) @@ -149,3 +149,11 @@ def test_uri(resolver, tmpdir, monkeypatch): # Check that the file was actually written assert prefs_e.filename.exists() assert prefs_e.filename == utils.Platform.user_prefs_filename() + + # Check if UriObj.__str__() is passing the uri contents + prefs_g = user_prefs.UriObj('test/uri') + assert prefs_g.__str__() == "test/uri" + + # Check UriObj.__str__ returns a string even if uri is None + prefs_g = user_prefs.UriObj() + assert isinstance(prefs_g.__str__(), str)