Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds PlatformDependent factory. #739

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
30 changes: 21 additions & 9 deletions src/rez/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rez import __version__
from rez.utils.data_utils import AttrDictWrapper, RO_AttrDictWrapper, \
convert_dicts, cached_property, cached_class_property, LazyAttributeMeta, \
deep_update, ModifyList, DelayLoad
deep_update, ModifyList, DelayLoad, HashableDict
from rez.utils.formatting import expandvars, expanduser
from rez.utils.logging_ import get_debug_printer
from rez.utils.scope import scoped_format
Expand Down Expand Up @@ -536,7 +536,7 @@ def override(self, key, value):
self.plugins.override(keys[1:], value)
else:
self.overrides[key] = value
self._uncache(key)
self._uncache(key, keep_plugins=True)

def is_overridden(self, key):
return (key in self.overrides)
Expand Down Expand Up @@ -636,7 +636,7 @@ def _get_plugin_completions(prefix_):
keys += _get_plugin_completions('')
return keys

def _uncache(self, key=None):
def _uncache(self, key=None, keep_plugins=False):
bfloch marked this conversation as resolved.
Show resolved Hide resolved
# deleting the attribute falls up back to the class attribute, which is
# the cached_property descriptor
if key and hasattr(self, key):
Expand All @@ -647,7 +647,7 @@ def _uncache(self, key=None):
if hasattr(self, "_data"):
delattr(self, "_data")

if hasattr(self, "plugins"):
if not keep_plugins and hasattr(self, "plugins"):
delattr(self, "plugins")

def _swap(self, other):
Expand Down Expand Up @@ -876,17 +876,24 @@ def _replace_config(other):


@lru_cache()
def _load_config_py(filepath):
def _load_config_py(filepath, fallback_platform_map):
from rez.utils.data_utils import Conditional, PlatformDependent, \
InConfigArchDependent, InConfigOsDependent
reserved = dict(
# Standard Python module variables
# Made available from within the module,
# and later excluded from the `Config` class
__name__=os.path.splitext(os.path.basename(filepath))[0],
__file__=filepath,
__fallback_platform_map=fallback_platform_map,

rez_version=__version__,
ModifyList=ModifyList,
DelayLoad=DelayLoad
DelayLoad=DelayLoad,
Conditional=Conditional,
PlatformDependent=PlatformDependent,
ArchDependent=InConfigArchDependent,
OsDependent=InConfigOsDependent,
)

g = reserved.copy()
Expand All @@ -903,14 +910,15 @@ def _load_config_py(filepath):
for k, v in g.items():
if k != '__builtins__' \
and not ismodule(v) \
and k not in reserved:
and k not in reserved \
and k != "__fallback_platform_map":
result[k] = v

return result


@lru_cache()
def _load_config_yaml(filepath):
def _load_config_yaml(filepath, _):
with open(filepath) as f:
content = f.read()
try:
Expand Down Expand Up @@ -942,7 +950,11 @@ def _load_config_from_filepaths(filepaths):
if not os.path.isfile(filepath_with_ext):
continue

data_ = loader(filepath_with_ext)
previous_platform_map = data.get("platform_map", None)
if previous_platform_map is not None:
previous_platform_map = HashableDict(previous_platform_map)

data_ = loader(filepath_with_ext, previous_platform_map)
deep_update(data, data_)
sourced_filepaths.append(filepath_with_ext)
break
Expand Down
12 changes: 12 additions & 0 deletions src/rez/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ class ConfigurationError(RezError):
pass


class ConditionalConfigurationError(ConfigurationError):
"""
A misconfiguration error due to a missing key in a Conditional.
"""
def __init__(self, missing_key, *args, **kwargs):
super(ConditionalConfigurationError, self).__init__(
"'{}' not found in Conditional options".format(missing_key),
*args,
**kwargs
)


class ResolveError(RezError):
"""A resolve-related error."""
pass
Expand Down
10 changes: 10 additions & 0 deletions src/rez/rezconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
(based on Python's os.path.sep). So for Linux paths, / should be used. On
Windows \ (unescaped) should be used.

Platform dependent configurations can be achieved via the PlatformDependent
factory. Read the documentation for details. For example:
default_shell = PlatformDependent(
{
"linux": "bash",
"windows": "powershell",
},
default="zsh"
)

Note: The comments in this file are extracted and turned into Wiki content. Pay
attention to the comment formatting and follow the existing style closely.
"""
Expand Down
33 changes: 33 additions & 0 deletions src/rez/tests/data/config/test_conditional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rez.utils.platform_ import platform_

platform_map = {
"arch": {
"^.*$": "IMPOSSIBLE_ARCH",
},
}

# Test fallback of Conditional
release_hooks = Conditional(
{
"Something": False,
},
key="3",
default=["foo"]
)

prune_failed_graph = ArchDependent({
"IMPOSSIBLE_ARCH": True,
})

# Fallback of OsDependent
warn_all = OsDependent({}, default=False)

plugins = PlatformDependent({
platform_.name: {
"release_hook": {
"emailer": {
"recipients": ["joe@here.com"]
}
}
}
})
4 changes: 4 additions & 0 deletions src/rez/tests/data/config/test_conditional_dependee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

dot_image_format = ArchDependent({
"IMPOSSIBLE_ARCH": "hello",
})
96 changes: 95 additions & 1 deletion src/rez/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from rez.exceptions import ConfigurationError
from rez.config import Config, get_module_root_config, _replace_config
from rez.system import system
from rez.utils.data_utils import RO_AttrDictWrapper
from rez.utils.data_utils import RO_AttrDictWrapper, PlatformDependent, \
ArchDependent, OsDependent
from rez.packages import get_developer_package
import os
import os.path
Expand Down Expand Up @@ -38,11 +39,13 @@ def _test_overrides(self, c):
c.override("build_directory", "floober")
c.override("plugins.release_vcs.tag_name", "bah")
c.override("plugins.release_hook.emailer.sender", "joe.bloggs")
c.override("implicit_packages", ["a list", "of values"])
bfloch marked this conversation as resolved.
Show resolved Hide resolved

self.assertEqual(c.debug_none, True)
self.assertEqual(c.build_directory, "floober")
self.assertEqual(c.plugins.release_vcs.tag_name, "bah")
self.assertEqual(c.plugins.release_hook.emailer.sender, "joe.bloggs")
self.assertListEqual(c.implicit_packages, ["a list", "of values"])

# second override
c.override("build_directory", "flabber")
Expand All @@ -59,6 +62,97 @@ def _test_overrides(self, c):

self._test_basic(c)

def test_conditional_overrides(self):
"""Test configuration with platform conditionals"""

from rez.utils.platform_ import platform_
platform_name = platform_.name
platform_arch = platform_.arch
platform_os = "IMPOSSIBLE_OS"

c = Config([self.root_config_file], locked=True)
c.override("platform_map", {
"os": {"^.*$": "IMPOSSIBLE_OS"},
})

c.validate_data()

# Missing valid key or fallback
with self.assertRaises(ConfigurationError):
c.override("debug_none", PlatformDependent({
"__not__valid__platform__": True,
}))

c.override("debug_none", PlatformDependent({
platform_name: True,
}))

c.override("build_directory", PlatformDependent({
platform_name: "floober"
}))

# Usage of fallback key
c.override("plugins.release_vcs.tag_name", PlatformDependent(
{ "something else": "Not sure", },
default="bah"
))

# Arch variant
c.override("plugins.release_hook.emailer.sender", ArchDependent({
platform_arch: "joe.bloggs",
}
))

# Os variant with explicit platform_map override
c.override(
"implicit_packages",
OsDependent(
{
platform_os: ["a list", "of values"],
},
platform_map=c.platform_map
)
)

c.validate_data()

self.assertEqual(c.debug_none, True)
self.assertEqual(c.build_directory, "floober")
self.assertEqual(c.plugins.release_vcs.tag_name, "bah")
self.assertEqual(c.plugins.release_hook.emailer.sender, "joe.bloggs")
self.assertListEqual(c.implicit_packages, ["a list", "of values"])

with self.assertRaises(ConfigurationError):
# Schema validation still works?
c.override("debug_none", PlatformDependent({
platform_name: "Wrong Type",
}))
c.validate_data()


def test_conditional_in_file(self):
"""Test package config overrides."""
conf = os.path.join(self.config_path, "test_conditional.py")
c = Config([self.root_config_file, conf])

self.assertListEqual(c.plugins.release_hook.emailer.recipients, ["joe@here.com"])
self.assertEqual(c.release_hooks, ["foo"])
self.assertEqual(c.prune_failed_graph, True)
self.assertEqual(c.warn_all, True)

def test_conidition_nested(self):
conf = os.path.join(self.config_path, "test_conditional.py")
conf_dependee = os.path.join(self.config_path, "test_conditional_dependee.py")
c = Config([conf, conf_dependee])
self.assertEqual(c.dot_image_format, "hello")

def test_conidition_nested_inbeween(self):
conf = os.path.join(self.config_path, "test_conditional.py")
conf_middle = os.path.join(self.config_path, "test2.py")
conf_dependee = os.path.join(self.config_path, "test_conditional_dependee.py")
c = Config([conf, conf_middle, conf_dependee])
self.assertEqual(c.dot_image_format, "hello")

def test_1(self):
"""Test just the root config file."""

Expand Down
23 changes: 23 additions & 0 deletions src/rez/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
unit tests for 'utils.filesystem' module
"""
import os
from collections import OrderedDict
from rez.tests.util import TestBase
from rez.utils import filesystem
from rez.utils.data_utils import HashableDict
from rez.utils.platform_ import Platform, platform_


Expand Down Expand Up @@ -46,6 +48,27 @@ def test_unix_case_insensistive_platform(self):
self.assertEqual(path, expects)


class TestDataUtils(TestBase):

def test_hashabledict(self):
# Make sure that hashes matches even on dicts with different order.

a = HashableDict(OrderedDict([("a", 1), ("b", 3)]))
b = HashableDict(OrderedDict([("b", 3), ("a", 1)]))
c = HashableDict(OrderedDict([("a", 1), ("c", 3)]))

self.assertEqual(a, b)
self.assertNotEqual(a, c)

# Immutable

with self.assertRaises(TypeError):
a["d"] = 5

self.assertEqual(a, b)



# Copyright 2013-2016 Allan Johns.
#
# This library is free software: you can redistribute it and/or
Expand Down
Loading