Skip to content

Commit

Permalink
Adding functionality for handling and expired URI
Browse files Browse the repository at this point in the history
I am addressing issue #45 which asks for functionality to deal with an
expired URI, which is found in the form of a locally saved json file
`user_prefs.json`.  We want the user to be notified that the URI is now
invalid and to allow them to enter a new URI or reuse the expired URI.
Along with this functionality are some code refactors that increase 
usability and readability

hab/user_prefs.py:
- Added a data class (UriObj) to handle passing URI related data 
to the cli
- Changed `uri_reason()` to `uri_check()` which now populates a UriObj
object with the URI and timedout data
- Adding a check to UriObj to make sure that __str__ returns a string 
even if the uri is None.

hab/cli.py:
- Added click.prompt to ask the user to enter a new URI plus giving 
them the option to use the previous URI if one has been saved.  
- Adding `err=True` to all click.echo and click.prompt calls in
UriArgument
- Added __uri_prompt() for better cli formatting and to add extra 
arguments for to support the changes.
- Moved some error echo calls behind the logger
- Cleaned up the CLI display for the user prompt for an expire URI
- Cleaned up the docstrings to reflect these changes.

tests/test_user_prefs.py:
- Modified the timedout tests so that they match the changes made
in user_prefs.py.  They now simply check for True or False against
UriObj.timedout
- Added a test to `UriObj.__str__()` to make sure that all aspects 
of the UriObj class are touched.

.gitignore:
- Adding ignores for .venv and .code-workspace

README.md:
- Added documentation for the expired URI changes and some spelling
fixes
  • Loading branch information
james-berkheimer authored and MHendricks committed Jun 9, 2023
1 parent ad77a53 commit 5f77fba
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ coverage.xml
dist/
htmlcov/
venv/
.venv/
version.py
*.sublime-project
*.sublime-workspace
*.code-workspace
.vscode/
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -
Expand All @@ -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
Expand Down Expand Up @@ -173,15 +175,15 @@ $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
* projectDummy/Sc001/S0001.00/Animation
* 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.

Expand Down Expand Up @@ -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

Expand Down
77 changes: 58 additions & 19 deletions hab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -303,6 +336,7 @@ def _cli(
logging.basicConfig(level=level)


# env command
@_cli.command(cls=UriHelpClass)
@click.argument("uri", cls=UriArgument)
@click.option(
Expand All @@ -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`.
Expand Down Expand Up @@ -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")
Expand All @@ -432,6 +469,7 @@ def echo_line(line):
click.echo(ret)


# activate command
@_cli.command(cls=UriHelpClass)
@click.argument("uri", cls=UriArgument)
@click.option(
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 25 additions & 14 deletions hab/user_prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
12 changes: 10 additions & 2 deletions tests/test_user_prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

0 comments on commit 5f77fba

Please sign in to comment.