Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
pantsbuild.pants / build_graph / build_configuration.py
Size: Mime:
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import logging
from collections import OrderedDict, namedtuple
from collections.abc import Iterable

from twitter.common.collections import OrderedSet

from pants.base.parse_context import ParseContext
from pants.build_graph.addressable import AddressableCallProxy
from pants.build_graph.build_file_aliases import BuildFileAliases
from pants.build_graph.target_addressable import TargetAddressable
from pants.engine.rules import RuleIndex
from pants.option.optionable import Optionable
from pants.util.memo import memoized_method

logger = logging.getLogger(__name__)


class BuildConfiguration:
    """Stores the types and helper functions exposed to BUILD files."""

    class ParseState(namedtuple("ParseState", ["parse_context", "parse_globals"])):
        @property
        def objects(self):
            return self.parse_context._storage.objects

    def __init__(self):
        self._target_by_alias = {}
        self._target_macro_factory_by_alias = {}
        self._exposed_object_by_alias = {}
        self._exposed_context_aware_object_factory_by_alias = {}
        self._optionables = OrderedSet()
        self._rules = OrderedSet()
        self._union_rules = OrderedDict()

    def registered_aliases(self) -> BuildFileAliases:
        """Return the registered aliases exposed in BUILD files.

        These returned aliases aren't so useful for actually parsing BUILD files.
        They are useful for generating things like http://pantsbuild.github.io/build_dictionary.html.

        :returns: A new BuildFileAliases instance containing this BuildConfiguration's registered alias
                  mappings.
        """
        target_factories_by_alias = self._target_by_alias.copy()
        target_factories_by_alias.update(self._target_macro_factory_by_alias)
        return BuildFileAliases(
            targets=target_factories_by_alias,
            objects=self._exposed_object_by_alias.copy(),
            context_aware_object_factories=self._exposed_context_aware_object_factory_by_alias.copy(),
        )

    def register_aliases(self, aliases):
        """Registers the given aliases to be exposed in parsed BUILD files.

        :param aliases: The BuildFileAliases to register.
        :type aliases: :class:`pants.build_graph.build_file_aliases.BuildFileAliases`
        """
        if not isinstance(aliases, BuildFileAliases):
            raise TypeError("The aliases must be a BuildFileAliases, given {}".format(aliases))

        for alias, target_type in aliases.target_types.items():
            self._register_target_alias(alias, target_type)

        for alias, target_macro_factory in aliases.target_macro_factories.items():
            self._register_target_macro_factory_alias(alias, target_macro_factory)

        for alias, obj in aliases.objects.items():
            self._register_exposed_object(alias, obj)

        for alias, context_aware_object_factory in aliases.context_aware_object_factories.items():
            self._register_exposed_context_aware_object_factory(alias, context_aware_object_factory)

    # TODO(John Sirois): Warn on alias override across all aliases since they share a global
    # namespace in BUILD files.
    # See: https://github.com/pantsbuild/pants/issues/2151
    def _register_target_alias(self, alias, target_type):
        if alias in self._target_by_alias:
            logger.debug("Target alias {} has already been registered. Overwriting!".format(alias))

        self._target_by_alias[alias] = target_type
        self.register_optionables(target_type.subsystems())

    def _register_target_macro_factory_alias(self, alias, target_macro_factory):
        if alias in self._target_macro_factory_by_alias:
            logger.debug(
                "TargetMacro alias {} has already been registered. Overwriting!".format(alias)
            )

        self._target_macro_factory_by_alias[alias] = target_macro_factory
        for target_type in target_macro_factory.target_types:
            self.register_optionables(target_type.subsystems())

    def _register_exposed_object(self, alias, obj):
        if alias in self._exposed_object_by_alias:
            logger.debug("Object alias {} has already been registered. Overwriting!".format(alias))

        self._exposed_object_by_alias[alias] = obj
        # obj doesn't implement any common base class, so we have to test for this attr.
        if hasattr(obj, "subsystems"):
            self.register_optionables(obj.subsystems())

    def _register_exposed_context_aware_object_factory(self, alias, context_aware_object_factory):
        if alias in self._exposed_context_aware_object_factory_by_alias:
            logger.debug(
                "This context aware object factory alias {} has already been registered. "
                "Overwriting!".format(alias)
            )

        self._exposed_context_aware_object_factory_by_alias[alias] = context_aware_object_factory

    def register_optionables(self, optionables):
        """Registers the given subsystem types.

        :param optionables: The Optionable types to register.
        :type optionables: :class:`collections.Iterable` containing
                           :class:`pants.option.optionable.Optionable` subclasses.
        """
        if not isinstance(optionables, Iterable):
            raise TypeError("The optionables must be an iterable, given {}".format(optionables))
        optionables = tuple(optionables)
        if not optionables:
            return

        invalid_optionables = [
            s for s in optionables if not isinstance(s, type) or not issubclass(s, Optionable)
        ]
        if invalid_optionables:
            raise TypeError(
                "The following items from the given optionables are not Optionable "
                "subclasses:\n\t{}".format("\n\t".join(str(i) for i in invalid_optionables))
            )

        self._optionables.update(optionables)

    def optionables(self):
        """Returns the registered Optionable types.

        :rtype set
        """
        return self._optionables

    def register_rules(self, rules):
        """Registers the given rules.

        param rules: The rules to register.
        :type rules: :class:`collections.Iterable` containing
                     :class:`pants.engine.rules.Rule` instances.
        """
        if not isinstance(rules, Iterable):
            raise TypeError("The rules must be an iterable, given {!r}".format(rules))

        # "Index" the rules to normalize them and expand their dependencies.
        normalized_rules = RuleIndex.create(rules).normalized_rules()
        indexed_rules = normalized_rules.rules
        union_rules = normalized_rules.union_rules

        # Store the rules and record their dependency Optionables.
        self._rules.update(indexed_rules)
        for union_base, new_members in union_rules.items():
            existing_members = self._union_rules.get(union_base, None)
            if existing_members is None:
                self._union_rules[union_base] = new_members
            else:
                existing_members.update(new_members)
        dependency_optionables = {
            do
            for rule in indexed_rules
            for do in rule.dependency_optionables
            if rule.dependency_optionables
        }
        self.register_optionables(dependency_optionables)

    def rules(self):
        """Returns the registered rules.

        :rtype: list
        """
        return list(self._rules)

    def union_rules(self):
        """Returns a mapping of registered union base types -> [OrderedSet of union member types].

        :rtype: OrderedDict
        """
        return self._union_rules

    @memoized_method
    def _get_addressable_factory(self, target_type, alias):
        return TargetAddressable.factory(target_type=target_type, alias=alias)

    def initialize_parse_state(self, build_file):
        """Creates a fresh parse state for the given build file.

        :param build_file: The BUILD file to set up a new ParseState for.
        :type build_file: :class:`pants.base.build_file.BuildFile`
        :returns: A fresh ParseState for parsing the given `build_file` with.
        :rtype: :class:`BuildConfiguration.ParseState`
        """
        # TODO(John Sirois): Introduce a factory method to seal the BuildConfiguration and add a check
        # there that all anonymous types are covered by context aware object factories that are
        # Macro instances.  Without this, we could have non-Macro context aware object factories being
        # asked to be a BuildFileTargetFactory when they are not (in SourceRoot registration context).
        # See: https://github.com/pantsbuild/pants/issues/2125
        type_aliases = self._exposed_object_by_alias.copy()
        parse_context = ParseContext(rel_path=build_file.spec_path, type_aliases=type_aliases)

        def create_call_proxy(tgt_type, tgt_alias=None):
            def registration_callback(address, addressable):
                parse_context._storage.add(addressable, name=address.target_name)

            addressable_factory = self._get_addressable_factory(tgt_type, tgt_alias)
            return AddressableCallProxy(
                addressable_factory=addressable_factory,
                build_file=build_file,
                registration_callback=registration_callback,
            )

        # Expose all aliased Target types.
        for alias, target_type in self._target_by_alias.items():
            proxy = create_call_proxy(target_type, alias)
            type_aliases[alias] = proxy

        # Expose aliases for exposed objects and targets in the BUILD file.
        parse_globals = type_aliases.copy()

        # Now its safe to add mappings from both the directly exposed and macro-created target types to
        # their call proxies for context awares and macros to use to manufacture targets by type
        # instead of by alias.
        for alias, target_type in self._target_by_alias.items():
            proxy = type_aliases[alias]
            type_aliases[target_type] = proxy

        for target_macro_factory in self._target_macro_factory_by_alias.values():
            for target_type in target_macro_factory.target_types:
                proxy = create_call_proxy(target_type)
                type_aliases[target_type] = proxy

        for alias, object_factory in self._exposed_context_aware_object_factory_by_alias.items():
            parse_globals[alias] = object_factory(parse_context)

        for alias, target_macro_factory in self._target_macro_factory_by_alias.items():
            parse_globals[alias] = target_macro_factory.target_macro(parse_context)

        return self.ParseState(parse_context, parse_globals)