diff --git a/docs/rez_sphinxext.py b/docs/rez_sphinxext.py index 2ce9189692..024fba07f4 100644 --- a/docs/rez_sphinxext.py +++ b/docs/rez_sphinxext.py @@ -4,6 +4,7 @@ import rez.cli._main import rez.cli._util +import rez.config import rez.rezconfig import docutils.nodes import sphinx.util.nodes @@ -262,9 +263,46 @@ def convert_rez_config_to_rst() -> list[str]: rst.append('') envvar = f'REZ_{varname.upper()}' - rst.append(f' .. envvar:: {envvar}') + setting_type = rez.config.config_schema._schema.get(varname) + + if varname == 'plugins': + # Plugins don't have a schema like the normal settings + # have, so let's not document environment variables since + # they effectively don't support them. + assert setting_type is None + continue + + assert setting_type is not None + + json_envvar = f'{envvar}_JSON' + rst.append(' Environment variables:') + rst.append('') + if not setting_type.env_var_json_only: + rst.append(f' .. envvar:: {envvar}') + rst.append(f' .. envvar:: {json_envvar}') rst.append('') - rst.append(f' The ``{envvar}`` environment variable can also be used to configure this.') + + if setting_type.env_var_json_only: + rst.append( + f' The non-JSON ``{envvar}`` environment variable is not supported.' + ) + rst.append('') + continue + + if issubclass(setting_type, rez.config.PathList): + rst.append( + f' Values in ``{envvar}`` must be separated with ``:`` on Unix-like ' + 'systems and ``;`` on Windows.' + ) + elif issubclass(setting_type, rez.config.StrList): + rst.append( + f' Values in ``{envvar}`` can be separated by commas or whitespace.' + ) + elif issubclass(setting_type, rez.config.Dict): + rst.append( + f' Dictionary values in ``{envvar}`` must use the format ' + '``k1:v1,k2:v2,...kN:vN``.' + ) rst.append('') return rst diff --git a/docs/source/configuring_rez.rst b/docs/source/configuring_rez.rst index f71a971764..4b5ea2b4d4 100644 --- a/docs/source/configuring_rez.rst +++ b/docs/source/configuring_rez.rst @@ -12,9 +12,11 @@ Settings are determined in the following way: :envvar:`REZ_CONFIG_FILE` environment variable. This can also be a path-like variable, to read from multiple configuration files; - The setting is further overridden if it is present in ``$HOME/.rezconfig`` or ``$HOME/.rezconfig.py``; +- The setting is overridden again if the environment variable :envvar:`REZ_XXX_JSON` is present, where ``XXX`` + is the uppercase version of the setting key. Its value must be JSON-encoded; - The setting is overridden again if the environment variable :envvar:`REZ_XXX` is present, where ``XXX`` is the uppercase version of the setting key. For example, :data:`.image_viewer` will be overridden by - :envvar:`REZ_IMAGE_VIEWER`. + :envvar:`REZ_IMAGE_VIEWER`. This form takes precedence if both environment variables are present; - This is a special case applied only during a package build or release. In this case, if the package definition file contains a "config" section, settings in this section will override all others. See :ref:`configuring-rez-package-overrides`. diff --git a/docs/source/environment.rst b/docs/source/environment.rst index 121b4427d1..3869f14ea1 100644 --- a/docs/source/environment.rst +++ b/docs/source/environment.rst @@ -219,6 +219,8 @@ operation of rez. Path to a rez configuration file. +.. _config-environment-variable-overrides: + .. envvar:: REZ_XXX For any given rez config entry (see ``rezconfig.py``), @@ -235,6 +237,7 @@ operation of rez. Same as :envvar:`REZ_XXX`, except that the format is a JSON string. This means that some more complex settings can be overridden, that aren't supported in the non-JSON case (:data:`package_filter` is an example). + If both forms are present, :envvar:`REZ_XXX` takes precedence. .. envvar:: REZ_DISABLE_HOME_CONFIG diff --git a/src/rez/config.py b/src/rez/config.py index d69895825a..668c7a0622 100644 --- a/src/rez/config.py +++ b/src/rez/config.py @@ -67,6 +67,10 @@ class Setting(object): """ schema: Validatable = Schema(object) + # Only set to True in subclasses when the non-JSON + # env var cannot be used by users. + env_var_json_only = False + def __init__(self, config, key) -> None: self.config = config self.key = key @@ -96,7 +100,8 @@ def _validate(self, data): if not self.config.locked: - # next, env-var + # next, env-var. Note that all settings support _JSON + # but not all support the non-JSON variant. value = os.getenv(self._env_var_name) if value is not None: if self.key in _deprecated_settings: @@ -108,6 +113,12 @@ def _validate(self, data): pre_formatted=True, filename=self._env_var_name, ) + if self.env_var_json_only: + raise ConfigurationError( + "The setting %r doesn't support the environment variable $%s, " + "use $%s_JSON instead." + % (self.key, self._env_var_name, self._env_var_name) + ) return self._parse_env_var(value) # next, JSON-encoded env-var @@ -180,6 +191,7 @@ class PipInstallRemaps(Setting): KEYS = ["record_path", "pip_install", "rez_install"] schema = Schema([{key: And(str, len) for key in KEYS}]) + env_var_json_only = True def validate(self, data: list) -> list: """Extended to substitute regex-escaped path tokens.""" @@ -302,6 +314,7 @@ class OptionalDictOrDictList(Setting): schema = Or(And(None, Use(lambda x: [])), And(dict, Use(lambda x: [x])), [dict]) + env_var_json_only = True class SuiteVisibility_(Str): diff --git a/src/rez/rezconfig.py b/src/rez/rezconfig.py index d596a993a2..268859ea2d 100644 --- a/src/rez/rezconfig.py +++ b/src/rez/rezconfig.py @@ -14,13 +14,15 @@ files are supported, separated by os.pathsep; 3) The setting is further overriden if it is present in $HOME/.rezconfig, UNLESS $REZ_DISABLE_HOME_CONFIG is 1; -4) The setting is overridden again if the environment variable $REZ_XXX is +4) The setting can also be overridden by the environment variable + $REZ_XXX_JSON, and in this case the string is expected to be a JSON-encoded + value; +5) The setting is overridden again if the environment variable $REZ_XXX is present, where XXX is the uppercase version of the setting key. For example, "image_viewer" will be overriden by $REZ_IMAGE_VIEWER. List values can be separated either with "," or blank space. Dict values are in the form - "k1:v1,k2:v2,kn:vn"; -5) The setting can also be overriden by the environment variable $REZ_XXX_JSON, - and in this case the string is expected to be a JSON-encoded value; + "k1:v1,k2:v2,kn:vn". This form takes precedence if both environment + variables are present; 6) This is a special case applied only during a package build or release. In this case, if the package definition file contains a "config" section, settings in this section will override all others. diff --git a/src/rez/tests/test_config.py b/src/rez/tests/test_config.py index 084e1f1617..edb61d0465 100644 --- a/src/rez/tests/test_config.py +++ b/src/rez/tests/test_config.py @@ -296,6 +296,33 @@ def test_8(self): print(error.stdout) raise + def test_9_json_only_environment_variable(self) -> None: + """Test the error when a JSON-only setting uses its plain env var.""" + for key in ("package_orderers", "pip_install_remaps"): + with self.subTest(key=key), restore_os_environ(): + env_var = "REZ_%s" % key.upper() + os.environ[env_var] = "invalid" + config = Config([self.root_config_file], locked=False) + + with self.assertRaises(ConfigurationError) as error: + getattr(config, key) + + self.assertEqual( + str(error.exception), + "The setting %r doesn't support the environment variable $%s, " + "use $%s_JSON instead." + % (key, env_var, env_var), + ) + + def test_10_environment_variable_precedence(self) -> None: + """Test that the plain environment variable takes precedence.""" + with restore_os_environ(): + os.environ["REZ_IMAGE_VIEWER"] = "plain" + os.environ["REZ_IMAGE_VIEWER_JSON"] = '"json"' + config = Config([self.root_config_file], locked=False) + + self.assertEqual(config.image_viewer, "plain") + class TestDeprecations(TestBase, TempdirMixin): @classmethod