summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbst-marge-bot <marge-bot@buildstream.build>2019-07-29 10:36:54 +0000
committerbst-marge-bot <marge-bot@buildstream.build>2019-07-29 10:36:54 +0000
commite97f0348744a8503c8c75a44d961d85b13a58d47 (patch)
tree79d3f219789df862ef446607dbab1b846c3b897f
parent7736e44adb29688c13da1f4c5bddf03bd878a0f3 (diff)
parentfed2996dd58733d5e048e401c40ccbd16a9bc6f2 (diff)
downloadbuildstream-e97f0348744a8503c8c75a44d961d85b13a58d47.tar.gz
Merge branch 'bschubert/node-enum' into 'master'
Add a 'as_enum' on Scalar nodes to help with constraining inputs See merge request BuildStream/buildstream!1487
-rw-r--r--.pylintrc1
-rwxr-xr-xsetup.py1
-rw-r--r--src/buildstream/_context.py34
-rw-r--r--src/buildstream/_frontend/app.py7
-rw-r--r--src/buildstream/_frontend/cli.py19
-rw-r--r--src/buildstream/_gitsourcebase.py13
-rw-r--r--src/buildstream/_options/optionpool.py19
-rw-r--r--src/buildstream/_project.py20
-rw-r--r--src/buildstream/_scheduler/jobs/job.py18
-rw-r--r--src/buildstream/_scheduler/queues/queue.py4
-rw-r--r--src/buildstream/_types.pyx82
-rw-r--r--src/buildstream/element.py9
-rw-r--r--src/buildstream/node.pxd2
-rw-r--r--src/buildstream/node.pyx77
-rw-r--r--src/buildstream/storage/directory.py4
-rw-r--r--src/buildstream/types.py108
16 files changed, 342 insertions, 76 deletions
diff --git a/.pylintrc b/.pylintrc
index cb4967a2f..0ed9280b7 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -8,6 +8,7 @@ extension-pkg-whitelist=
buildstream._loader._loader,
buildstream._loader.loadelement,
buildstream._loader.types,
+ buildstream._types,
buildstream._utils,
buildstream._variables,
buildstream._yaml
diff --git a/setup.py b/setup.py
index 141fb0539..19779fb69 100755
--- a/setup.py
+++ b/setup.py
@@ -407,6 +407,7 @@ register_cython_module("buildstream._loader._loader")
register_cython_module("buildstream._loader.loadelement")
register_cython_module("buildstream._loader.types", dependencies=["buildstream.node"])
register_cython_module("buildstream._yaml", dependencies=["buildstream.node"])
+register_cython_module("buildstream._types")
register_cython_module("buildstream._utils")
register_cython_module("buildstream._variables", dependencies=["buildstream.node"])
diff --git a/src/buildstream/_context.py b/src/buildstream/_context.py
index c18f9426d..c1a3b0619 100644
--- a/src/buildstream/_context.py
+++ b/src/buildstream/_context.py
@@ -29,6 +29,7 @@ from ._platform import Platform
from ._artifactcache import ArtifactCache
from ._sourcecache import SourceCache
from ._cas import CASCache, CASQuota, CASCacheUsage
+from .types import _CacheBuildTrees, _SchedulerErrorAction
from ._workspaces import Workspaces, WorkspaceProjectCache
from .node import Node
from .sandbox import SandboxRemote
@@ -298,8 +299,7 @@ class Context():
self.pull_buildtrees = cache.get_bool('pull-buildtrees')
# Load cache build trees configuration
- self.cache_buildtrees = _node_get_option_str(
- cache, 'cache-buildtrees', ['always', 'auto', 'never'])
+ self.cache_buildtrees = cache.get_enum('cache-buildtrees', _CacheBuildTrees)
# Load logging config
logging = defaults.get_mapping('logging')
@@ -323,8 +323,7 @@ class Context():
'on-error', 'fetchers', 'builders',
'pushers', 'network-retries'
])
- self.sched_error_action = _node_get_option_str(
- scheduler, 'on-error', ['continue', 'quit', 'terminate'])
+ self.sched_error_action = scheduler.get_enum('on-error', _SchedulerErrorAction)
self.sched_fetchers = scheduler.get_int('fetchers')
self.sched_builders = scheduler.get_int('builders')
self.sched_pushers = scheduler.get_int('pushers')
@@ -503,30 +502,3 @@ class Context():
if self._casquota is None:
self._casquota = CASQuota(self)
return self._casquota
-
-
-# _node_get_option_str()
-#
-# Like Node.get_scalar().as_str(), but also checks value is one of the allowed option
-# strings. Fetches a value from a dictionary node, and makes sure it's one of
-# the pre-defined options.
-#
-# Args:
-# node (dict): The dictionary node
-# key (str): The key to get a value for in node
-# allowed_options (iterable): Only accept these values
-#
-# Returns:
-# The value, if found in 'node'.
-#
-# Raises:
-# LoadError, when the value is not of the expected type, or is not found.
-#
-def _node_get_option_str(node, key, allowed_options):
- result_node = node.get_scalar(key)
- result = result_node.as_str()
- if result not in allowed_options:
- provenance = result_node.get_provenance()
- raise LoadError("{}: {} should be one of: {}".format(provenance, key, ", ".join(allowed_options)),
- LoadErrorReason.INVALID_DATA)
- return result
diff --git a/src/buildstream/_frontend/app.py b/src/buildstream/_frontend/app.py
index 76d3b8dde..379ed4b7f 100644
--- a/src/buildstream/_frontend/app.py
+++ b/src/buildstream/_frontend/app.py
@@ -37,6 +37,7 @@ from .._exceptions import BstError, StreamError, LoadError, LoadErrorReason, App
from .._message import Message, MessageType, unconditional_messages
from .._stream import Stream
from .._versions import BST_FORMAT_VERSION
+from ..types import _SchedulerErrorAction
from .. import node
# Import frontend assets
@@ -597,11 +598,11 @@ class App():
# Handle non interactive mode setting of what to do when a job fails.
if not self._interactive_failures:
- if self.context.sched_error_action == 'terminate':
+ if self.context.sched_error_action == _SchedulerErrorAction.TERMINATE:
self.stream.terminate()
- elif self.context.sched_error_action == 'quit':
+ elif self.context.sched_error_action == _SchedulerErrorAction.QUIT:
self.stream.quit()
- elif self.context.sched_error_action == 'continue':
+ elif self.context.sched_error_action == _SchedulerErrorAction.CONTINUE:
pass
return
diff --git a/src/buildstream/_frontend/cli.py b/src/buildstream/_frontend/cli.py
index 12b9439cf..276f81a6a 100644
--- a/src/buildstream/_frontend/cli.py
+++ b/src/buildstream/_frontend/cli.py
@@ -10,10 +10,25 @@ from .. import _yaml
from .._exceptions import BstError, LoadError, AppError
from .._versions import BST_FORMAT_VERSION
from .complete import main_bashcomplete, complete_path, CompleteUnhandled
+from ..types import _CacheBuildTrees, _SchedulerErrorAction
from ..utils import _get_compression, UtilError
##################################################################
+# Helper classes and methods for Click #
+##################################################################
+
+class FastEnumType(click.Choice):
+
+ def __init__(self, enum):
+ self._enum = enum
+ super().__init__(enum.values())
+
+ def convert(self, value, param, ctx):
+ return self._enum(super().convert(value, param, ctx))
+
+
+##################################################################
# Override of click's main entry point #
##################################################################
@@ -234,7 +249,7 @@ def print_version(ctx, param, value):
type=click.Path(file_okay=False, readable=True),
help="Project directory (default: current directory)")
@click.option('--on-error', default=None,
- type=click.Choice(['continue', 'quit', 'terminate']),
+ type=FastEnumType(_SchedulerErrorAction),
help="What to do when an error is encountered")
@click.option('--fetchers', type=click.INT, default=None,
help="Maximum simultaneous download tasks")
@@ -270,7 +285,7 @@ def print_version(ctx, param, value):
@click.option('--pull-buildtrees', is_flag=True, default=None,
help="Include an element's build tree when pulling remote element artifacts")
@click.option('--cache-buildtrees', default=None,
- type=click.Choice(['always', 'auto', 'never']),
+ type=FastEnumType(_CacheBuildTrees),
help="Cache artifact build tree content on creation")
@click.pass_context
def cli(context, **kwargs):
diff --git a/src/buildstream/_gitsourcebase.py b/src/buildstream/_gitsourcebase.py
index fb6010b1e..68eb4c2fb 100644
--- a/src/buildstream/_gitsourcebase.py
+++ b/src/buildstream/_gitsourcebase.py
@@ -32,6 +32,7 @@ from configparser import RawConfigParser
from .source import Source, SourceError, SourceFetcher
from .types import Consistency, CoreWarnings
from . import utils
+from .types import FastEnum
from .utils import move_atomic, DirectoryExistsError
GIT_MODULES = '.gitmodules'
@@ -42,6 +43,11 @@ WARN_UNLISTED_SUBMODULE = "unlisted-submodule"
WARN_INVALID_SUBMODULE = "invalid-submodule"
+class _RefFormat(FastEnum):
+ SHA1 = "sha1"
+ GIT_DESCRIBE = "git-describe"
+
+
# Because of handling of submodules, we maintain a _GitMirror
# for the primary git source and also for each submodule it
# might have at a given time
@@ -156,7 +162,7 @@ class _GitMirror(SourceFetcher):
cwd=self.mirror)
ref = output.rstrip('\n')
- if self.source.ref_format == 'git-describe':
+ if self.source.ref_format == _RefFormat.GIT_DESCRIBE:
# Prefix the ref with the closest tag, if available,
# to make the ref human readable
exit_code, output = self.source.check_output(
@@ -394,10 +400,7 @@ class _GitSourceBase(Source):
self.mirror = self.BST_MIRROR_CLASS(self, '', self.original_url, ref, tags=tags, primary=True)
self.tracking = node.get_str('track', None)
- self.ref_format = node.get_str('ref-format', 'sha1')
- if self.ref_format not in ['sha1', 'git-describe']:
- provenance = node.get_scalar('ref-format').get_provenance()
- raise SourceError("{}: Unexpected value for ref-format: {}".format(provenance, self.ref_format))
+ self.ref_format = node.get_enum('ref-format', _RefFormat, _RefFormat.SHA1)
# At this point we now know if the source has a ref and/or a track.
# If it is missing both then we will be unable to track or build.
diff --git a/src/buildstream/_options/optionpool.py b/src/buildstream/_options/optionpool.py
index d6b1b1614..a0730c617 100644
--- a/src/buildstream/_options/optionpool.py
+++ b/src/buildstream/_options/optionpool.py
@@ -22,6 +22,7 @@ import jinja2
from .._exceptions import LoadError, LoadErrorReason
from ..node import MappingNode, SequenceNode, _assert_symbol_name
+from ..types import FastEnum
from .optionbool import OptionBool
from .optionenum import OptionEnum
from .optionflags import OptionFlags
@@ -40,6 +41,15 @@ _OPTION_TYPES = {
}
+class OptionTypes(FastEnum):
+ BOOL = OptionBool.OPTION_TYPE
+ ENUM = OptionEnum.OPTION_TYPE
+ FLAG = OptionFlags.OPTION_TYPE
+ ELT_MASK = OptionEltMask.OPTION_TYPE
+ ARCH = OptionArch.OPTION_TYPE
+ OS = OptionOS.OPTION_TYPE
+
+
class OptionPool():
def __init__(self, element_path):
@@ -80,13 +90,8 @@ class OptionPool():
# Assert that the option name is a valid symbol
_assert_symbol_name(option_name, "option name", ref_node=option_definition, allow_dashes=False)
- opt_type_name = option_definition.get_str('type')
- try:
- opt_type = _OPTION_TYPES[opt_type_name]
- except KeyError:
- p = option_definition.get_scalar('type').get_provenance()
- raise LoadError("{}: Invalid option type '{}'".format(p, opt_type_name),
- LoadErrorReason.INVALID_DATA)
+ opt_type_name = option_definition.get_enum('type', OptionTypes)
+ opt_type = _OPTION_TYPES[opt_type_name.value]
option = opt_type(option_name, option_definition, self)
self._options[option_name] = option
diff --git a/src/buildstream/_project.py b/src/buildstream/_project.py
index f4a7466de..9428ab4f6 100644
--- a/src/buildstream/_project.py
+++ b/src/buildstream/_project.py
@@ -41,6 +41,7 @@ from ._projectrefs import ProjectRefs, ProjectRefStorage
from ._versions import BST_FORMAT_VERSION
from ._loader import Loader
from .element import Element
+from .types import FastEnum
from ._message import Message, MessageType
from ._includes import Includes
from ._workspaces import WORKSPACE_PROJECT_FILE
@@ -50,6 +51,13 @@ from ._workspaces import WORKSPACE_PROJECT_FILE
_PROJECT_CONF_FILE = 'project.conf'
+# List of all places plugins can come from
+class PluginOrigins(FastEnum):
+ CORE = "core"
+ LOCAL = "local"
+ PIP = "pip"
+
+
# HostMount()
#
# A simple object describing the behavior of
@@ -862,14 +870,8 @@ class Project():
'origin', 'sources', 'elements',
'package-name', 'path',
]
- allowed_origins = ['core', 'local', 'pip']
origin.validate_keys(allowed_origin_fields)
- origin_value = origin.get_str('origin')
- if origin_value not in allowed_origins:
- raise LoadError("Origin '{}' is not one of the allowed types"
- .format(origin_value), LoadErrorReason.INVALID_YAML)
-
# Store source versions for checking later
source_versions = origin.get_mapping('sources', default={})
for key in source_versions.keys():
@@ -888,7 +890,9 @@ class Project():
# Store the origins if they're not 'core'.
# core elements are loaded by default, so storing is unnecessary.
- if origin.get_str('origin') != 'core':
+ origin_value = origin.get_enum('origin', PluginOrigins)
+
+ if origin_value != PluginOrigins.CORE:
self._store_origin(origin, 'sources', plugin_source_origins)
self._store_origin(origin, 'elements', plugin_element_origins)
@@ -928,7 +932,7 @@ class Project():
if group in origin_node:
del origin_node[group]
- if origin_node.get_str('origin') == 'local':
+ if origin_node.get_enum('origin', PluginOrigins) == PluginOrigins.LOCAL:
path = self.get_path_from_node(origin.get_scalar('path'),
check_is_dir=True)
# paths are passed in relative to the project, but must be absolute
diff --git a/src/buildstream/_scheduler/jobs/job.py b/src/buildstream/_scheduler/jobs/job.py
index 7975488ed..c651d5206 100644
--- a/src/buildstream/_scheduler/jobs/job.py
+++ b/src/buildstream/_scheduler/jobs/job.py
@@ -20,7 +20,6 @@
# Tristan Maat <tristan.maat@codethink.co.uk>
# System imports
-import enum
import os
import sys
import signal
@@ -32,6 +31,7 @@ import multiprocessing
# BuildStream toplevel imports
from ..._exceptions import ImplError, BstError, set_last_task_error, SkipJob
from ..._message import Message, MessageType, unconditional_messages
+from ...types import FastEnum
from ... import _signals, utils
from .jobpickler import pickle_child_job
@@ -39,8 +39,7 @@ from .jobpickler import pickle_child_job
# Return code values shutdown of job handling child processes
#
-@enum.unique
-class _ReturnCode(enum.IntEnum):
+class _ReturnCode(FastEnum):
OK = 0
FAIL = 1
PERM_FAIL = 2
@@ -52,8 +51,7 @@ class _ReturnCode(enum.IntEnum):
# The job completion status, passed back through the
# complete callbacks.
#
-@enum.unique
-class JobStatus(enum.Enum):
+class JobStatus(FastEnum):
# Job succeeded
OK = 0
@@ -80,8 +78,7 @@ class Process(multiprocessing.Process):
self._sentinel = self._popen.sentinel
-@enum.unique
-class _MessageType(enum.Enum):
+class _MessageType(FastEnum):
LOG_MESSAGE = 1
ERROR = 2
RESULT = 3
@@ -443,6 +440,11 @@ class Job():
def _parent_child_completed(self, pid, returncode):
self._parent_shutdown()
+ try:
+ returncode = _ReturnCode(returncode)
+ except KeyError: # An unexpected return code was returned, let's fail permanently
+ returncode = _ReturnCode.PERM_FAIL
+
# We don't want to retry if we got OK or a permanent fail.
retry_flag = returncode == _ReturnCode.FAIL
@@ -834,7 +836,7 @@ class ChildJob():
def _child_shutdown(self, exit_code):
self._queue.close()
assert isinstance(exit_code, _ReturnCode)
- sys.exit(int(exit_code))
+ sys.exit(exit_code.value)
# _child_message_handler()
#
diff --git a/src/buildstream/_scheduler/queues/queue.py b/src/buildstream/_scheduler/queues/queue.py
index 538b2b9d1..79f1fa44f 100644
--- a/src/buildstream/_scheduler/queues/queue.py
+++ b/src/buildstream/_scheduler/queues/queue.py
@@ -21,7 +21,6 @@
# System imports
import os
from collections import deque
-from enum import Enum
import heapq
import traceback
@@ -32,12 +31,13 @@ from ..resources import ResourceType
# BuildStream toplevel imports
from ..._exceptions import BstError, ImplError, set_last_task_error
from ..._message import Message, MessageType
+from ...types import FastEnum
# Queue status for a given element
#
#
-class QueueStatus(Enum):
+class QueueStatus(FastEnum):
# The element is not yet ready to be processed in the queue.
PENDING = 1
diff --git a/src/buildstream/_types.pyx b/src/buildstream/_types.pyx
new file mode 100644
index 000000000..0f8d3690c
--- /dev/null
+++ b/src/buildstream/_types.pyx
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2018 Bloomberg LP
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
+# Jim MacArthur <jim.macarthur@codethink.co.uk>
+# Benjamin Schubert <bschubert15@bloomberg.net>
+
+# MetaFastEnum()
+#
+# This is a reemplementation of MetaEnum, in order to get a faster implementation of Enum.
+#
+# Enum turns out to be very slow and would add a noticeable slowdown when we try to add them to the codebase.
+# We therefore reimplement a subset of the `Enum` functionality that keeps it compatible with normal `Enum`.
+# That way, any place in the code base that access a `FastEnum`, can normally also accept an `Enum`. The reverse
+# is not correct, since we only implement a subset of `Enum`.
+class MetaFastEnum(type):
+ def __new__(mcs, name, bases, dct):
+ if name == "FastEnum":
+ return type.__new__(mcs, name, bases, dct)
+
+ assert len(bases) == 1, "Multiple inheritance with Fast enums is not currently supported."
+
+ dunder_values = {}
+ normal_values = {}
+
+ parent_keys = bases[0].__dict__.keys()
+
+ assert "__class__" not in dct.keys(), "Overriding '__class__' is not allowed on 'FastEnum' classes"
+
+ for key, value in dct.items():
+ if key.startswith("__") and key.endswith("__"):
+ dunder_values[key] = value
+ else:
+ assert key not in parent_keys, "Overriding 'FastEnum.{}' is not allowed. ".format(key)
+ normal_values[key] = value
+
+ kls = type.__new__(mcs, name, bases, dunder_values)
+ mcs.set_values(kls, normal_values)
+
+ return kls
+
+ @classmethod
+ def set_values(mcs, kls, data):
+ value_to_entry = {}
+
+ assert len(set(data.values())) == len(data.values()), "Values for {} are not unique".format(kls)
+ assert len(set(type(value) for value in data.values())) <= 1, \
+ "Values of {} are of heterogeneous types".format(kls)
+
+ for key, value in data.items():
+ new_value = object.__new__(kls)
+ object.__setattr__(new_value, "value", value)
+ object.__setattr__(new_value, "name", key)
+
+ type.__setattr__(kls, key, new_value)
+
+ value_to_entry[value] = new_value
+
+ type.__setattr__(kls, "_value_to_entry", value_to_entry)
+
+ def __repr__(self):
+ return "<fastenum '{}'>".format(self.__name__)
+
+ def __setattr__(self, key, value):
+ raise ValueError("Adding new values dynamically is not supported")
+
+ def __iter__(self):
+ return iter(self._value_to_entry.values())
diff --git a/src/buildstream/element.py b/src/buildstream/element.py
index ffdd1511e..df96d04ba 100644
--- a/src/buildstream/element.py
+++ b/src/buildstream/element.py
@@ -102,7 +102,7 @@ from .plugin import Plugin
from .sandbox import SandboxFlags, SandboxCommandError
from .sandbox._config import SandboxConfig
from .sandbox._sandboxremote import SandboxRemote
-from .types import Consistency, CoreWarnings, Scope, _KeyStrength
+from .types import Consistency, CoreWarnings, Scope, _CacheBuildTrees, _KeyStrength
from ._artifact import Artifact
from .storage.directory import Directory
@@ -1617,8 +1617,8 @@ class Element(Plugin):
# download when it's not needed.
buildroot = self.get_variable('build-root')
cache_buildtrees = context.cache_buildtrees
- if cache_buildtrees != 'never':
- always_cache_buildtrees = cache_buildtrees == 'always'
+ if cache_buildtrees != _CacheBuildTrees.NEVER:
+ always_cache_buildtrees = cache_buildtrees == _CacheBuildTrees.ALWAYS
sandbox._set_build_directory(buildroot, always=always_cache_buildtrees)
if not self.BST_RUN_COMMANDS:
@@ -1701,7 +1701,8 @@ class Element(Plugin):
# result. Element types without a build-root dir will be cached
# with an empty buildtreedir regardless of this configuration.
- if cache_buildtrees == 'always' or (cache_buildtrees == 'auto' and not build_success):
+ if cache_buildtrees == _CacheBuildTrees.ALWAYS or \
+ (cache_buildtrees == _CacheBuildTrees.AUTO and not build_success):
try:
sandbox_build_dir = sandbox_vroot.descend(
*self.get_variable('build-root').lstrip(os.sep).split(os.sep))
diff --git a/src/buildstream/node.pxd b/src/buildstream/node.pxd
index 18520146d..02e95d06f 100644
--- a/src/buildstream/node.pxd
+++ b/src/buildstream/node.pxd
@@ -46,6 +46,7 @@ cdef class MappingNode(Node):
# Public Methods
cpdef bint get_bool(self, str key, default=*) except *
+ cpdef object get_enum(self, str key, object constraint, object default=*)
cpdef int get_int(self, str key, default=*) except *
cpdef MappingNode get_mapping(self, str key, default=*)
cpdef Node get_node(self, str key, list allowed_types=*, bint allow_none=*)
@@ -78,6 +79,7 @@ cdef class ScalarNode(Node):
# Public Methods
cpdef bint as_bool(self) except *
+ cpdef object as_enum(self, object constraint)
cpdef int as_int(self) except *
cpdef str as_str(self)
cpdef bint is_none(self)
diff --git a/src/buildstream/node.pyx b/src/buildstream/node.pyx
index ea63151b9..98a785868 100644
--- a/src/buildstream/node.pyx
+++ b/src/buildstream/node.pyx
@@ -329,6 +329,45 @@ cdef class ScalarNode(Node):
.format(provenance, path, bool.__name__, self.value),
LoadErrorReason.INVALID_DATA)
+ cpdef object as_enum(self, object constraint):
+ """Get the value of the node as an enum member from `constraint`
+
+ The constraint must be a :class:`buildstream.types.FastEnum` or a plain python Enum.
+
+ For example you could do:
+
+ .. code-block:: python
+
+ from buildstream.types import FastEnum
+
+ class SupportedCompressions(FastEnum):
+ NONE = "none"
+ GZIP = "gzip"
+ XZ = "xz"
+
+
+ x = config.get_scalar('compress').as_enum(SupportedCompressions)
+
+ if x == SupportedCompressions.GZIP:
+ print("Using GZIP")
+
+ Args:
+ constraint (:class:`buildstream.types.FastEnum` or :class:`Enum`): an enum from which to extract the value
+ for the current node.
+
+ Returns:
+ :class:`FastEnum` or :class:`Enum`: the value contained in the node, as a member of `constraint`
+ """
+ try:
+ return constraint(self.value)
+ except ValueError:
+ provenance = self.get_provenance()
+ path = provenance._toplevel._find(self)[-1]
+ valid_values = [str(v.value) for v in constraint]
+ raise LoadError("{}: Value of '{}' should be one of '{}'".format(
+ provenance, path, ", ".join(valid_values)),
+ LoadErrorReason.INVALID_DATA)
+
cpdef int as_int(self) except *:
"""Get the value of the node as an integer.
@@ -511,6 +550,44 @@ cdef class MappingNode(Node):
cdef ScalarNode scalar = self.get_scalar(key, default)
return scalar.as_bool()
+ cpdef object get_enum(self, str key, object constraint, object default=_sentinel):
+ """Get the value of the node as an enum member from `constraint`
+
+ Args:
+ key (str): key for which to get the value
+ constraint (:class:`buildstream.types.FastEnum` or :class:`Enum`): an enum from which to extract the value
+ for the current node.
+ default (object): default value to return if `key` is not in the mapping
+
+ Raises:
+ :class:`buildstream._exceptions.LoadError`: if the value is not is not found or not part of the
+ provided enum.
+
+ Returns:
+ :class:`buildstream.types.Enum` or :class:`Enum`: the value contained in the node, as a member of
+ `constraint`
+ """
+ cdef object value = self.value.get(key, _sentinel)
+
+ if value is _sentinel:
+ if default is _sentinel:
+ provenance = self.get_provenance()
+ raise LoadError("{}: Dictionary did not contain expected key '{}'".format(provenance, key),
+ LoadErrorReason.INVALID_DATA)
+
+ if default is None:
+ return None
+ else:
+ return constraint(default)
+
+ if type(value) is not ScalarNode:
+ provenance = value.get_provenance()
+ raise LoadError("{}: Value of '{}' is not of the expected type 'scalar'"
+ .format(provenance, key),
+ LoadErrorReason.INVALID_DATA)
+
+ return (<ScalarNode> value).as_enum(constraint)
+
cpdef int get_int(self, str key, object default=_sentinel) except *:
"""get_int(key, default=sentinel)
diff --git a/src/buildstream/storage/directory.py b/src/buildstream/storage/directory.py
index d32ac0063..5039e0af9 100644
--- a/src/buildstream/storage/directory.py
+++ b/src/buildstream/storage/directory.py
@@ -31,9 +31,9 @@ See also: :ref:`sandboxing`.
"""
-from enum import Enum
from .._exceptions import BstError, ErrorDomain
+from ..types import FastEnum
from ..utils import BST_ARBITRARY_TIMESTAMP
@@ -196,7 +196,7 @@ class Directory():
#
# Type of file or directory entry.
#
-class _FileType(Enum):
+class _FileType(FastEnum):
# Directory
DIRECTORY = 1
diff --git a/src/buildstream/types.py b/src/buildstream/types.py
index 6f6262e41..0635f310e 100644
--- a/src/buildstream/types.py
+++ b/src/buildstream/types.py
@@ -25,10 +25,67 @@ Foundation types
"""
-from enum import Enum
+from ._types import MetaFastEnum
-class Scope(Enum):
+class FastEnum(metaclass=MetaFastEnum):
+ """
+ A reimplementation of a subset of the `Enum` functionality, which is far quicker than `Enum`.
+
+ :class:`enum.Enum` attributes accesses can be really slow, and slow down the execution noticeably.
+ This reimplementation doesn't suffer the same problems, but also does not reimplement everything.
+ """
+
+ name = None
+ """The name of the current Enum entry, same as :func:`enum.Enum.name`
+ """
+
+ value = None
+ """The value of the current Enum entry, same as :func:`enum.Enum.value`
+ """
+
+ _value_to_entry = dict() # A dict of all values mapping to the entries in the enum
+
+ @classmethod
+ def values(cls):
+ """Get all the possible values for the enum.
+
+ Returns:
+ list: the list of all possible values for the enum
+ """
+ return cls._value_to_entry.keys()
+
+ def __new__(cls, value):
+ try:
+ return cls._value_to_entry[value]
+ except KeyError:
+ if type(value) is cls: # pylint: disable=unidiomatic-typecheck
+ return value
+ raise ValueError("Unknown enum value: {}".format(value))
+
+ def __eq__(self, other):
+ if self.__class__ is not other.__class__:
+ raise ValueError("Unexpected comparison between {} and {}".format(self, repr(other)))
+ # Enums instances are unique, so creating an instance with the same value as another will just
+ # send back the other one, hence we can use an identity comparison, which is much faster than '=='
+ return self is other
+
+ def __ne__(self, other):
+ if self.__class__ is not other.__class__:
+ raise ValueError("Unexpected comparison between {} and {}".format(self, repr(other)))
+ return self is not other
+
+ def __hash__(self):
+ return hash(id(self))
+
+ def __str__(self):
+ return "{}.{}".format(self.__class__.__name__, self.name)
+
+ def __reduce__(self):
+ return self.__class__, (self.value,)
+
+
+class Scope(FastEnum):
"""Defines the scope of dependencies to include for a given element
when iterating over the dependency graph in APIs like
:func:`Element.dependencies() <buildstream.element.Element.dependencies>`
@@ -56,7 +113,7 @@ class Scope(Enum):
"""
-class Consistency():
+class Consistency(FastEnum):
"""Defines the various consistency states of a :class:`.Source`.
"""
@@ -80,6 +137,16 @@ class Consistency():
Sources have a cached unstaged copy in the source directory.
"""
+ def __ge__(self, other):
+ if self.__class__ is not other.__class__:
+ raise ValueError("Unexpected comparison between {} and {}".format(self, repr(other)))
+ return self.value >= other.value
+
+ def __lt__(self, other):
+ if self.__class__ is not other.__class__:
+ raise ValueError("Unexpected comparison between {} and {}".format(self, repr(other)))
+ return self.value < other.value
+
class CoreWarnings():
"""CoreWarnings()
@@ -116,7 +183,7 @@ class CoreWarnings():
#
# Strength of cache key
#
-class _KeyStrength(Enum):
+class _KeyStrength(FastEnum):
# Includes strong cache keys of all build dependencies and their
# runtime dependencies.
@@ -125,3 +192,36 @@ class _KeyStrength(Enum):
# Includes names of direct build dependencies but does not include
# cache keys of dependencies.
WEAK = 2
+
+
+# _SchedulerErrorAction()
+#
+# Actions the scheduler can take on error
+#
+class _SchedulerErrorAction(FastEnum):
+
+ # Continue building the rest of the tree
+ CONTINUE = "continue"
+
+ # finish ongoing work and quit
+ QUIT = "quit"
+
+ # Abort immediately
+ TERMINATE = "terminate"
+
+
+# _CacheBuildTrees()
+#
+# When to cache build trees
+#
+class _CacheBuildTrees(FastEnum):
+
+ # Always store build trees
+ ALWAYS = "always"
+
+ # Store build trees when they might be useful for BuildStream
+ # (eg: on error, to allow for a shell to debug that)
+ AUTO = "auto"
+
+ # Never cache build trees
+ NEVER = "never"