diff options
author | bst-marge-bot <marge-bot@buildstream.build> | 2019-07-29 10:36:54 +0000 |
---|---|---|
committer | bst-marge-bot <marge-bot@buildstream.build> | 2019-07-29 10:36:54 +0000 |
commit | e97f0348744a8503c8c75a44d961d85b13a58d47 (patch) | |
tree | 79d3f219789df862ef446607dbab1b846c3b897f | |
parent | 7736e44adb29688c13da1f4c5bddf03bd878a0f3 (diff) | |
parent | fed2996dd58733d5e048e401c40ccbd16a9bc6f2 (diff) | |
download | buildstream-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-- | .pylintrc | 1 | ||||
-rwxr-xr-x | setup.py | 1 | ||||
-rw-r--r-- | src/buildstream/_context.py | 34 | ||||
-rw-r--r-- | src/buildstream/_frontend/app.py | 7 | ||||
-rw-r--r-- | src/buildstream/_frontend/cli.py | 19 | ||||
-rw-r--r-- | src/buildstream/_gitsourcebase.py | 13 | ||||
-rw-r--r-- | src/buildstream/_options/optionpool.py | 19 | ||||
-rw-r--r-- | src/buildstream/_project.py | 20 | ||||
-rw-r--r-- | src/buildstream/_scheduler/jobs/job.py | 18 | ||||
-rw-r--r-- | src/buildstream/_scheduler/queues/queue.py | 4 | ||||
-rw-r--r-- | src/buildstream/_types.pyx | 82 | ||||
-rw-r--r-- | src/buildstream/element.py | 9 | ||||
-rw-r--r-- | src/buildstream/node.pxd | 2 | ||||
-rw-r--r-- | src/buildstream/node.pyx | 77 | ||||
-rw-r--r-- | src/buildstream/storage/directory.py | 4 | ||||
-rw-r--r-- | src/buildstream/types.py | 108 |
16 files changed, 342 insertions, 76 deletions
@@ -8,6 +8,7 @@ extension-pkg-whitelist= buildstream._loader._loader, buildstream._loader.loadelement, buildstream._loader.types, + buildstream._types, buildstream._utils, buildstream._variables, buildstream._yaml @@ -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" |