Exemplo n.º 1
0
class ResourceWrapper(six.with_metaclass(AttributeForwardMeta, object)):
    """An object that wraps a resource instance.

    A resource wrapper is useful for two main reasons. First, we can wrap
    several different resources with the one class, giving them a common
    interface. This is useful when the same resource can be loaded from various
    different sources (perhaps a database and the filesystem for example), and
    further common functionality needs to be supplied.

    Second, some resource attributes can be derived from the resource's
    variables, which means the resource's data doesn't need to be loaded to get
    these attributes. The wrapper can provide its own properties that do this,
    avoiding unnecessary data loads.

    You must subclass this class and provide `keys` - the list of attributes in
    the resource that you want to expose in the wrapper. The `schema_keys`
    function is provided to help get a list of keys from a resource schema.
    """
    keys = None

    def __init__(self, resource):
        self.wrapped = resource

    @property
    def resource(self):
        return self.wrapped

    @property
    def handle(self):
        return self.resource.handle

    @property
    def data(self):
        return self.resource._data

    def validated_data(self):
        return self.resource.validated_data()

    def validate_data(self):
        self.resource.validate_data()

    def __eq__(self, other):
        return (
            self.__class__ == other.__class__
            and self.resource == other.resource
        )

    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, str(self.resource))

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.resource)

    def __hash__(self):
        return hash((self.__class__, self.resource))
Exemplo n.º 2
0
class Resource(six.with_metaclass(LazyAttributeMeta, object)):
    """Abstract base class for a data resource.

    A resource is an object uniquely identified by a 'key' (the resource type),
    and a dict of variables. For example, a very simple banking system might
    have a resource type with key 'account.checking', and a single variable
    'account_owner' that uniquely identifies each checking account.

    Resources may have a schema, which describes the data associated with the
    resource. For example, a checking account might have a current balance (an
    integer) and a social security number (also an integer).

    Keys in a resource's schema are mapped onto the resource class. So a
    checking account instance 'account' would have attributes 'account.balance',
    'account.ssn' etc. Attributes are lazily validated, using the schema, on
    first access.

    A resource's data is loaded lazily, on first attribute access. This,
    combined with lazy attribute validation, means that many resources can be
    iterated, while potentially expensive operations (data loading, attribute
    validation) are put off as long as possible.

    Note:
        You can access the entire validated resource data dict using the
        `validated_data` function, and test full validation using `validate_data`.

    Attributes:
        key (str): Unique identifier of the resource type.
        schema (Schema): Schema for the resource data. Must validate a dict.
            Can be None, in which case the resource does not load any data.
        schema_error (Exception): The exception type to raise on key
            validation failure.
    """
    key = None
    schema = None
    schema_error = Exception

    @classmethod
    def normalize_variables(cls, variables):
        """Give subclasses a chance to standardize values for certain variables
        """
        return variables

    def __init__(self, variables=None):
        self.variables = self.normalize_variables(variables or {})

    @cached_property
    def handle(self):
        """Get the resource handle."""
        return ResourceHandle(self.key, self.variables)

    @cached_property
    def _data(self):
        if not self.schema:
            return None

        data = self._load()
        if config.debug("resources"):
            print_debug("Loaded resource: %s" % str(self))
        return data

    def get(self, key, default=None):
        """Get the value of a resource variable."""
        return self.variables.get(key, default)

    def __str__(self):
        return "%s%r" % (self.key, self.variables)

    def __repr__(self):
        return "%s(%r)" % (self.__class__.__name__, self.variables)

    def __hash__(self):
        return hash((self.__class__, self.handle))

    def __eq__(self, other):
        return (self.handle == other.handle)

    def _load(self):
        """Load the data associated with the resource.

        You are not expected to cache this data - the resource system does this
        for you.

        If `schema` is None, this signifies that the resource does not load any
        data. In this case you don't need to implement this function - it will
        never be called.

        Returns:
            dict.
        """
        raise NotImplementedError
Exemplo n.º 3
0
class Config(six.with_metaclass(LazyAttributeMeta, object)):
    """Rez configuration settings.

    You should call the `create_config` function, rather than constructing a
    `Config` object directly.

    Config files are merged with other config files to create a `Config`
    instance. The 'rezconfig' file in rez acts as the master - other config
    files update the master configuration to create the final config. See the
    comments at the top of 'rezconfig' for more details.
    """
    schema = config_schema
    schema_error = ConfigurationError

    def __init__(self, filepaths, overrides=None, locked=False):
        """Create a config.

        Args:
            filepaths (list of str): List of config files to load.
            overrides (dict): A dict containing settings that override all
                others. Nested settings are overridden with nested dicts.
            locked: If True, settings overrides in environment variables are
                ignored.
        """
        self.filepaths = filepaths
        self._sourced_filepaths = None
        self.overrides = overrides or {}
        self.locked = locked

    def get(self, key, default=None):
        """Get the value of a setting."""
        return getattr(self, key, default)

    def copy(self, overrides=None, locked=False):
        """Create a separate copy of this config."""
        other = copy.copy(self)

        if overrides is not None:
            other.overrides = overrides

        other.locked = locked

        other._uncache()
        return other

    def override(self, key, value):
        """Set a setting to the given value.

        Note that `key` can be in dotted form, eg
        'plugins.release_hook.emailer.sender'.
        """
        keys = key.split('.')
        if len(keys) > 1:
            if keys[0] != "plugins":
                raise AttributeError("no such setting: %r" % key)
            self.plugins.override(keys[1:], value)
        else:
            self.overrides[key] = value
            self._uncache(key)

    def is_overridden(self, key):
        return (key in self.overrides)

    def remove_override(self, key):
        """Remove a setting override, if one exists."""
        keys = key.split('.')
        if len(keys) > 1:
            raise NotImplementedError
        elif key in self.overrides:
            del self.overrides[key]
            self._uncache(key)

    def warn(self, key):
        """Returns True if the warning setting is enabled."""
        return (not self.quiet and not self.warn_none
                and (self.warn_all or getattr(self, "warn_%s" % key)))

    def debug(self, key):
        """Returns True if the debug setting is enabled."""
        return (not self.quiet and not self.debug_none
                and (self.debug_all or getattr(self, "debug_%s" % key)))

    def debug_printer(self, key):
        """Returns a printer object suitably enabled based on the given key."""
        enabled = self.debug(key)
        return get_debug_printer(enabled)

    @cached_property
    def sourced_filepaths(self):
        """Get the list of files actually sourced to create the config.

        Note:
            `self.filepaths` refers to the filepaths used to search for the
            configs, which does dot necessarily match the files used. For example,
            some files may not exist, while others are chosen as rezconfig.py in
            preference to rezconfig, rezconfig.yaml.

        Returns:
            List of str: The sourced files.
        """
        _ = self._data  # noqa; force a config load
        return self._sourced_filepaths

    @cached_property
    def plugins(self):
        """Plugin settings are loaded lazily, to avoid loading the plugins
        until necessary."""
        plugin_data = self._data.get("plugins", {})
        return _PluginConfigs(plugin_data)

    @property
    def data(self):
        """Returns the entire configuration as a dict.

        Note that this will force all plugins to be loaded.
        """
        d = {}
        for key in self._data:
            if key == "plugins":
                d[key] = self.plugins.data()
            else:
                try:
                    d[key] = getattr(self, key)
                except AttributeError:
                    pass  # unknown key, just leave it unchanged
        return d

    @property
    def nonlocal_packages_path(self):
        """Returns package search paths with local path removed."""
        paths = self.packages_path[:]
        if self.local_packages_path in paths:
            paths.remove(self.local_packages_path)
        return paths

    def get_completions(self, prefix):
        def _get_plugin_completions(prefix_):
            from rez.utils.data_utils import get_object_completions
            words = get_object_completions(instance=self.plugins,
                                           prefix=prefix_,
                                           instance_types=(dict,
                                                           AttrDictWrapper))
            return ["plugins." + x for x in words]

        toks = prefix.split('.')
        if len(toks) > 1:
            if toks[0] == "plugins":
                prefix_ = '.'.join(toks[1:])
                return _get_plugin_completions(prefix_)
            return []
        else:
            keys = (
                [x for x in self._schema_keys if isinstance(x, basestring)] +
                ["plugins"])
            keys = [x for x in keys if x.startswith(prefix)]
            if keys == ["plugins"]:
                keys += _get_plugin_completions('')
            return keys

    def _uncache(self, key=None):
        # deleting the attribute falls up back to the class attribute, which is
        # the cached_property descriptor
        if key and hasattr(self, key):
            delattr(self, key)

        # have to uncache entire data/plugins dict also, since overrides may
        # have been changed
        if hasattr(self, "_data"):
            delattr(self, "_data")

        if hasattr(self, "plugins"):
            delattr(self, "plugins")

    def _swap(self, other):
        """Swap this config with another.

        This is used by the unit tests to swap the config to one that is
        shielded from any user config updates. Do not use this method unless
        you have good reason.
        """
        self.__dict__, other.__dict__ = other.__dict__, self.__dict__

    def _validate_key(self, key, value, key_schema):
        if isinstance(value, DelayLoad):
            value = value.get_value()

        if type(key_schema) is type and issubclass(key_schema, Setting):
            key_schema = key_schema(self, key)
        elif not isinstance(key_schema, Schema):
            key_schema = Schema(key_schema)

        return key_schema.validate(value)

    @cached_property
    def _data_without_overrides(self):
        data, self._sourced_filepaths = _load_config_from_filepaths(
            self.filepaths)
        return data

    @cached_property
    def _data(self):
        data = copy.deepcopy(self._data_without_overrides)

        # need to do this regardless of overrides, in order to flatten
        # ModifyList instances
        deep_update(data, self.overrides)

        return data

    @classmethod
    def _create_main_config(cls, overrides=None):
        """See comment block at top of 'rezconfig' describing how the main
        config is assembled."""
        filepaths = []
        filepaths.append(get_module_root_config())
        filepath = os.getenv("REZ_CONFIG_FILE")
        if filepath:
            filepaths.extend(filepath.split(os.pathsep))

        filepath = os.path.expanduser("~/.rezconfig")
        filepaths.append(filepath)

        return Config(filepaths, overrides)

    def __str__(self):
        keys = (x for x in self.schema._schema if isinstance(x, basestring))
        return "%r" % sorted(list(keys) + ["plugins"])

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, str(self))

    # -- dynamic defaults

    def _get_tmpdir(self):
        from rez.utils.platform_ import platform_
        return platform_.tmpdir

    def _get_context_tmpdir(self):
        from rez.utils.platform_ import platform_
        return platform_.tmpdir

    def _get_image_viewer(self):
        from rez.utils.platform_ import platform_
        return platform_.image_viewer

    def _get_editor(self):
        from rez.utils.platform_ import platform_
        return platform_.editor

    def _get_difftool(self):
        from rez.utils.platform_ import platform_
        return platform_.difftool

    def _get_terminal_emulator_command(self):
        from rez.utils.platform_ import platform_
        return platform_.terminal_emulator_command

    def _get_new_session_popen_args(self):
        from rez.utils.platform_ import platform_
        return platform_.new_session_popen_args
Exemplo n.º 4
0
class VariantResourceHelper(six.with_metaclass(_Metas, VariantResource)):
    """Helper class for implementing variants that inherit properties from their
    parent package.

    Since a variant overlaps so much with a package, here we use the forwarding
    metaclass to forward our parent package's attributes onto ourself (with some
    exceptions - eg 'variants', 'requires'). This is a common enough pattern
    that it's supplied here for other repository plugins to use.
    """

    # Note: lazy key validation doesn't happen in this class, it just fowards on
    # attributes from the package. But LazyAttributeMeta does still use this
    # schema to create other class attributes, such as `validate_data`.
    schema = variant_schema

    # forward Package attributes onto ourself
    keys = schema_keys(package_schema) - set(["variants"])

    def _uri(self):
        index = self.index
        idxstr = '' if index is None else str(index)
        return "%s[%s]" % (self.parent.uri, idxstr)

    def _subpath(self, ignore_shortlinks=False):
        if self.index is None:
            return None

        if self.parent.hashed_variants:
            vars_str = str(list(map(str, self.variant_requires)))
            h = sha1(vars_str.encode("utf8"))
            hashdir = h.hexdigest()

            if (not ignore_shortlinks) and \
                    config.use_variant_shortlinks and \
                    self.base is not None:

                # search for matching shortlink and use that
                path = os.path.join(self.base,
                                    config.variant_shortlinks_dirname)

                if os.path.exists(path):
                    actual_root = os.path.join(self.base, hashdir)
                    linkname = find_matching_symlink(path, actual_root)

                    if linkname:
                        return os.path.join(config.variant_shortlinks_dirname,
                                            linkname)

            return hashdir
        else:
            dirs = [x.safe_str() for x in self.variant_requires]
            subpath = os.path.join(*dirs)
            return subpath

    def _root(self, ignore_shortlinks=False):
        if self.base is None:
            return None
        elif self.index is None:
            return self.base
        else:
            subpath = self._subpath(ignore_shortlinks=ignore_shortlinks)
            root = os.path.join(self.base, subpath)
            return root

    @cached_property
    def variant_requires(self):
        index = self.index
        if index is None:
            return []
        else:
            try:
                return self.parent.variants[index] or []
            except (IndexError, TypeError):
                raise ResourceError(
                    "Unexpected error - variant %s cannot be found in its "
                    "parent package %s" % (self.uri, self.parent.uri))

    @property
    def wrapped(self):  # forward Package attributes onto ourself
        return self.parent

    def _load(self):
        # doesn't have its own data, forwards on from parent instead
        return None