diff --git a/README.md b/README.md index 84012c3..898e85e 100644 --- a/README.md +++ b/README.md @@ -933,12 +933,26 @@ defined as a two part list where the first item is the name of the created alias command. The second argument is the actual command to run and configuration definition. -Ultimately all alias definitions are turned into dictionaries like `as_dict`, but -you can also define aliases as lists of strings or a single string. It's -recommended that you use a list of strings for any commands that require multiple -arguments, for more details see args documentation in +Ultimately all alias definitions are turned into [dictionaries](#complex-aliases) +like `as_dict`, but you can also define aliases as lists of strings or a single +string. It's recommended that you use a list of strings for any commands that +require multiple arguments, for more details see args documentation in [subprocess.Popen](https://docs.python.org/3/library/subprocess.html#subprocess.Popen). +When the aliases are converted to dicts, they automatically get a "distro" key +added to them. This contains a two item tuple containing the `distro_name` and +`version`. This is preserved when frozen. You can use this to track down what +distro created a [duplicate alias](#duplicate-definitions). This is also useful +if you need to tie a farm job to a specific version of a dcc based on the active +hab setup. +```py +import hab +import os + +resolver = hab.Resolver.instance() +cfg = resolver.resolve(os.environ["HAB_URI"]) +version = cfg.aliases["houdinicore"]["distro"][1] +``` #### Complex Aliases diff --git a/hab/parsers/distro_version.py b/hab/parsers/distro_version.py index bbe830b..990ccb9 100644 --- a/hab/parsers/distro_version.py +++ b/hab/parsers/distro_version.py @@ -1,7 +1,7 @@ from packaging.version import InvalidVersion, Version from .. import NotSet -from ..errors import InvalidVersionError, _IgnoredVersionError +from ..errors import HabError, InvalidVersionError, _IgnoredVersionError from .distro import Distro from .hab_base import HabBase from .meta import hab_property @@ -114,18 +114,46 @@ def load(self, filename): # Fill in the DistroVersion specific settings before calling super data = self._load(filename) - self.aliases = data.get("aliases", NotSet) - # Store any alias_mods, they will be processed later when flattening - self._alias_mods = data.get("alias_mods", NotSet) - # The name should be the version == specifier. self.distro_name = data.get("name") self.name = "{}=={}".format(self.distro_name, self.version) + self.aliases = self.standardize_aliases(data.get("aliases", NotSet)) + # Store any alias_mods, they will be processed later when flattening + self._alias_mods = data.get("alias_mods", NotSet) + data = super().load(filename, data=data) return data + def standardize_aliases(self, aliases): + """Process a raw aliases dict adding distro information. + + Converts any non-dict alias definitions into dicts and adds the "distro" + tuple containing `(distro_name, version)`. Does nothing if passed NotSet. + + Returns: + dict: The same aliases object that was passed in. If it was a dict + the original dict's contents are modified. + """ + if aliases is NotSet: + return aliases + + version_info = (self.distro_name, str(self.version)) + for platform in aliases.values(): + for alias in platform: + # Ensure that we always have a dictionary for aliases + if not isinstance(alias[1], dict): + alias[1] = dict(cmd=alias[1]) + if "distro" in alias[1]: + raise HabError( + 'The "distro" value on an alias dict is reserved. You ' + "can not set this manually." + ) + # Store the distro information on each alias dict. + alias[1]["distro"] = version_info + return aliases + @hab_property() def version(self): return super().version diff --git a/hab/parsers/flat_config.py b/hab/parsers/flat_config.py index afd6b02..62495e5 100644 --- a/hab/parsers/flat_config.py +++ b/hab/parsers/flat_config.py @@ -100,10 +100,8 @@ def _process_version(self, version, existing=None): continue host_platform = utils.Platform.name() - # Ensure that we always have a dictionary for aliases + # Ensure the aliases are formatted and variables expanded data = version.format_environment_value(aliases[i]) - if not isinstance(data, dict): - data = dict(cmd=data) mods = self._alias_mods.get(alias_name, []) if "environment" not in data and not mods: diff --git a/hab/parsers/hab_base.py b/hab/parsers/hab_base.py index 69241b0..30a24a9 100644 --- a/hab/parsers/hab_base.py +++ b/hab/parsers/hab_base.py @@ -441,10 +441,13 @@ def filename(self, filename): self._dirname = self._filename.parent def format_environment_value(self, value, ext=None, platform=None): - """Apply standard formatting to environment variable values. + """Apply standard formatting to string values. + + If passed a list, tuple, dict, recursively calls this function on them + converting any strings found. Any bool or int values are not modified. Args: - value (str): The string to format + value: The string to format. ext (str, optional): Language passed to ``hab.formatter.Formatter`` for special formatters. In most cases this should not be used. platform (str, optional): Convert path values from the current @@ -463,6 +466,12 @@ def format_environment_value(self, value, ext=None, platform=None): self.format_environment_value(v, ext=ext, platform=platform) for v in value ] + elif isinstance(value, tuple): + # Format the individual items if a tuple of args is used. + return tuple( + self.format_environment_value(v, ext=ext, platform=platform) + for v in value + ) elif isinstance(value, dict): # Format the values each dictionary pair return { diff --git a/tests/distros/all_settings/0.1.0.dev1/invalid.hab.json b/tests/distros/all_settings/0.1.0.dev1/invalid.hab.json new file mode 100644 index 0000000..c8b080d --- /dev/null +++ b/tests/distros/all_settings/0.1.0.dev1/invalid.hab.json @@ -0,0 +1,22 @@ +{ + "name": "all_settings", + "version": "0.1.0.dev1", + "aliases": { + "windows": [ + [ + "maya", + { + "cmd": "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe", + "distro": ["all_settings", "0.1.0"] + } + ], + [ + "mayapy", + { + "cmd": "C:\\Program Files\\Autodesk\\Maya2020\\bin\\mayapy.exe", + "distro": ["all_settings", "0.1.0"] + } + ] + ] + } +} diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 487c351..f0851e4 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -12,6 +12,7 @@ from hab import NotSet, utils from hab.errors import ( DuplicateJsonError, + HabError, InvalidVersionError, ReservedVariableNameError, _IgnoredVersionError, @@ -26,6 +27,9 @@ def test_distro_parse(config_root, resolver): path = config_root / "distros" / "all_settings" / "0.1.0.dev1" / ".hab.json" app.load(path) check = json.load(path.open()) + # Add dynamic alias settings like "distro" to the testing reference. + # That should never be defined in the raw alias json data. + app.standardize_aliases(check["aliases"]) assert "{name}=={version}".format(**check) == app.name assert Version(check["version"]) == app.version @@ -48,6 +52,18 @@ def test_distro_parse(config_root, resolver): assert app.version == Version("2020.0") +def test_distro_exceptions(config_root, uncached_resolver): + """Check that a exception is raised if you define "distro" on an alias.""" + forest = {} + app = DistroVersion(forest, uncached_resolver) + # This file is used to test this feature and otherwise should be ignored. + path = config_root / "distros" / "all_settings" / "0.1.0.dev1" / "invalid.hab.json" + with pytest.raises( + HabError, match=r'The "distro" value on an alias dict is reserved.' + ): + app.load(path) + + def test_distro_version_resolve(config_root, resolver, helpers, monkeypatch, tmpdir): """Check the various methods for DistroVersion.version to be populated."""