Skip to content

Commit

Permalink
Add per-file formatting variable support
Browse files Browse the repository at this point in the history
This allows reusing a variable in multiple places inside of the file to
make it easier for automated tools to edit the file.
  • Loading branch information
MHendricks committed Feb 6, 2024
1 parent 782d791 commit acb456b
Show file tree
Hide file tree
Showing 9 changed files with 467 additions and 15 deletions.
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,21 +769,58 @@ and `Sc11`. The URI of `not_a_project/Sc101` would end up using `default/Sc1`. T
`not_a_project/Sc110` would use `default/Sc11`. The URI `not_a_project/Sc200` would
use `default`.

### Variable Formatting
### Str Formatting

The configuration environment variables and aliases can be formatted using str.format syntax.
The configuration environment variables and aliases can be formatted using
`str.format`. Hab extends the python [Format Specification Mini-Language](https://docs.python.org/3/library/string.html#formatspec)
with these extra features:
* `{ANYTHING!e}`: `!e` is a special conversion flag for Environment variables. This will
be replaced with the correct shell environment variable. For bash it becomes `$ANYTHING`,
in power shell `$env:ANYTHING`, and in command prompt `%ANYTHING%`. `ANYTHING` is the name
of the environment variable.

Currently supported variables:
#### Hab specific variables

Hab defines some specific variables. These are used when parsing an individual json file:
* `{relative_root}`: The directory name of the .json config file. Think of this as the relative path
`.` when using the command line, but this is a clear indication that it needs to be
replaced with the dirname and not left alone.
* `{ANYTHING!e}`: `!e` is a special conversion flag for Environment variables. This will
be replaced with the correct shell environment variable. For bash it becomes `$ANYTHING`,
in power shell `$env:ANYTHING`, and in command prompt `%ANYTHING%`. ANYTHING is the name
of the environment variable.
* `{;}`: This is replaced with the path separator for the shell. Ie `:` for bash, and `;`
on windows(including bash).

#### Custom variables

You can define custom variables in a json file by defining a "variables" dictionary.
You can use these keys in other parts of that file. An `ReservedVariableNameError`
will be raised if you try to replace hab specific variables like `relative_root` and `;`.

```json5
{
"name": "maya2024",
"variables": {
"maya_root_linux": "/usr/autodesk/maya2024/bin",
"maya_root_windows": "C:/Program Files/Autodesk/Maya2024/bin"
},
"aliases": {
"linux": [
["maya", {"cmd": "{maya_root_linux}/maya"}]
],
"windows": [
["maya", {"cmd": "{maya_root_windows}\\maya.exe"}]
]
}
}
```

The [maya2024](tests/distros/maya2024/2024.0/.hab.json) testing example shows a
way to centralize the path to the Maya bin directory. This way automated tools
can easily change the install directory of maya if it needs to be installed
into a custom location on specific workstations, or remote computers.

Note: Using hard coded paths like `maya_root_windows` should be avoided unless
you really can't use `relative_root`. `relative_root` is more portable across
workstation setups as it doesn't require any modifications when the path changes.

### Min_Verbosity

[Config](#config) and [aliases](#hiding-aliases) can be hidden depending on the
Expand Down
17 changes: 17 additions & 0 deletions hab/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,20 @@ def __str__(self):
if self.error:
ret = f"[{type(self.error).__name__}] {self.error}\n{ret}"
return ret


class ReservedVariableNameError(HabError):
"""Raised if a custom variable uses a reserved variable name."""

_reserved_variable_names = {"relative_root", ";"}
"""A set of variable names hab reserved for hab use and should not be defined
by custom variables."""

def __init__(self, invalid, filename):
self.filename = filename
msg = (
f"{', '.join(sorted(invalid))!r} are reserved variable name(s) for "
"hab and can not be used in the variables section. "
f"Filename: '{self.filename}'"
)
super().__init__(msg)
35 changes: 31 additions & 4 deletions hab/parsers/hab_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from packaging.version import Version

from .. import NotSet, utils
from ..errors import DuplicateJsonError, HabError
from ..errors import DuplicateJsonError, HabError, ReservedVariableNameError
from ..formatter import Formatter
from ..site import MergeDict
from ..solvers import Solver
Expand Down Expand Up @@ -48,6 +48,7 @@ def __init__(self, forest, resolver, filename=None, parent=None, root_paths=None
self._filename = None
self._dirname = None
self._distros = NotSet
self._variables = NotSet
self._uri = NotSet
self.parent = parent
self.root_paths = set()
Expand Down Expand Up @@ -472,9 +473,14 @@ def format_environment_value(self, value, ext=None, platform=None):
# Just return boolean values, no need to format
return value

# Expand and format any variables like "relative_root" using the current
# platform for paths.
kwargs = dict(relative_root=utils.path_forward_slash(self.dirname))
# Include custom variables in the format dictionary.
kwargs = {}
if self.variables:
kwargs.update(self.variables)

# Custom variables do not override hab variables, ensure they are set
# to the correct value.
kwargs["relative_root"] = utils.path_forward_slash(self.dirname)
ret = Formatter(ext).format(value, **kwargs)

# Use site.the platform_path_maps to convert the result to the target platform
Expand Down Expand Up @@ -553,6 +559,8 @@ def load(self, filename, data=None):
# Check for NotSet so sub-classes can set values before calling super
if self.name is NotSet:
self.name = data["name"]
if self.variables is NotSet:
self.variables = data.get("variables", NotSet)
if "version" in data and self.version is NotSet:
self.version = data.get("version")
if self.distros is NotSet:
Expand Down Expand Up @@ -706,6 +714,25 @@ def _apply(data):
if alias_name:
_apply(self.aliases[alias_name].get("environment", {}))

@hab_property(verbosity=3)
def variables(self):
"""A configurable dict of reusable key/value pairs to use when formatting
text strings in the rest of the json file. This value is stored in the
`variables` dictionary of the json file.
"""
return self._variables

@variables.setter
def variables(self, variables):
# Raise an exception if a reserved variable name is used.
if variables and isinstance(variables, dict):
invalid = ReservedVariableNameError._reserved_variable_names.intersection(
variables
)
if invalid:
raise ReservedVariableNameError(invalid, self.filename)
self._variables = variables

@property
def version(self):
"""A `packaging.version.Version` representing the version of this object."""
Expand Down
8 changes: 8 additions & 0 deletions tests/configs/app/app_maya_2024.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "2024",
"context": ["app", "maya"],
"inherits": true,
"distros": [
"maya2024"
]
}
29 changes: 28 additions & 1 deletion tests/configs/project_a/project_a.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,32 @@
"distros": [
"maya2020",
"houdini18.5"
]
],
"environment": {
"os_specific": true,
"linux": {
"set": {
"OCIO": "{mount_linux}/project_a/cfg/ocio/v0001/config.ocio",
"HOUDINI_OTLSCAN_PATH": [
"{mount_linux}/project_a/cfg/hdas",
"{mount_linux}/_shared/cfg/hdas",
"&"
]
}
},
"windows": {
"set": {
"OCIO": "{mount_windows}/project_a/cfg/ocio/v0001/config.ocio",
"HOUDINI_OTLSCAN_PATH": [
"{mount_windows}/project_a/cfg/hdas",
"{mount_windows}/_shared/cfg/hdas",
"&"
]
}
}
},
"variables": {
"mount_linux": "/blur/g",
"mount_windows": "G:"
}
}
79 changes: 79 additions & 0 deletions tests/distros/maya2024/2024.0/.hab.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"name": "maya2024",
"variables": {
"maya_root_linux": "/usr/autodesk/maya2024/bin",
"maya_root_windows": "C:/Program Files/Autodesk/Maya2024/bin"
},
"aliases": {
"windows": [
[
"maya", {
"cmd": "{maya_root_windows}/maya.exe"
}
],
[
"mayapy", {
"cmd": "{maya_root_windows}/mayapy.exe",
"min_verbosity": {"global": 2}
}
],
[
"maya24", {
"cmd": "{maya_root_windows}/maya.exe",
"min_verbosity": {"global": 1}
}
],
[
"mayapy24", {
"cmd": "{maya_root_windows}/mayapy.exe",
"min_verbosity": {"global": 2}
}
],
[
"pip", {
"cmd": [
"{maya_root_windows}/mayapy.exe",
"-m",
"pip"
],
"min_verbosity": {"global": 2}
}
]
],
"linux": [
[
"maya", {
"cmd": "{maya_root_linux}/maya"
}
],
[
"mayapy", {
"cmd": "{maya_root_linux}/mayapy",
"min_verbosity": {"global": 2}
}
],
[
"maya24", {
"cmd": "{maya_root_linux}/maya",
"min_verbosity": {"global": 1}
}
],
[
"mayapy24", {
"cmd": "{maya_root_linux}/mayapy",
"min_verbosity": {"global": 2}
}
],
[
"pip", {
"cmd": [
"{maya_root_linux}/mayapy",
"-m",
"pip"
],
"min_verbosity": {"global": 2}
}
]
]
}
}
Loading

0 comments on commit acb456b

Please sign in to comment.