def __init__(self, name='Project', rule_namespace=None, module_resolver=None, modules=None): """Initializes an empty project. Args: name: A human-readable name for the project that will be used for logging. rule_namespace: Rule namespace to use when loading modules. If omitted a default one is used. module_resolver: A module resolver to use when attempt to dynamically resolve modules by path. modules: A list of modules to add to the project. Raises: NameError: The name given is not valid. """ self.name = name if rule_namespace: self.rule_namespace = rule_namespace else: self.rule_namespace = RuleNamespace() self.rule_namespace.discover() if module_resolver: self.module_resolver = module_resolver else: self.module_resolver = StaticModuleResolver() self.modules = {} if modules and len(modules): self.add_modules(modules)
def __init__(self, path, rule_namespace=None, modes=None): """Initializes a loader. Args: path: File-system path to the module. rule_namespace: Rule namespace to use for rule definitions. """ self.path = path self.rule_namespace = rule_namespace if not self.rule_namespace: self.rule_namespace = RuleNamespace() self.rule_namespace.discover() self.modes = {} if modes: for mode in modes: if self.modes.has_key(mode): raise KeyError('Duplicate mode "%s" defined' % (mode)) self.modes[mode] = True self.code_str = None self.code_ast = None self.code_obj = None self._current_scope = None
class Project(object): """Project type that contains rules. Projects, once constructed, are designed to be immutable. Many duplicate build processes may run over the same project instance and all expect it to be in the state it was when first created. """ def __init__(self, name='Project', rule_namespace=None, module_resolver=None, modules=None): """Initializes an empty project. Args: name: A human-readable name for the project that will be used for logging. rule_namespace: Rule namespace to use when loading modules. If omitted a default one is used. module_resolver: A module resolver to use when attempt to dynamically resolve modules by path. modules: A list of modules to add to the project. Raises: NameError: The name given is not valid. """ self.name = name if rule_namespace: self.rule_namespace = rule_namespace else: self.rule_namespace = RuleNamespace() self.rule_namespace.discover() if module_resolver: self.module_resolver = module_resolver else: self.module_resolver = StaticModuleResolver() self.modules = {} if modules and len(modules): self.add_modules(modules) def add_module(self, module): """Adds a module to the project. Args: module: A module to add. Raises: KeyError: A module with the given name already exists in the project. """ self.add_modules([module]) def add_modules(self, modules): """Adds a list of modules to the project. Args: modules: A list of modules to add. Raises: KeyError: A module with the given name already exists in the project. """ for module in modules: if self.modules.get(module.path, None): raise KeyError('A module with the path "%s" is already defined' % ( module.path)) for module in modules: self.modules[module.path] = module def get_module(self, module_path): """Gets a module by path. Args: module_path: Name of the module to find. Returns: The module with the given path or None if it was not found. """ return self.modules.get(module_path, None) def module_list(self): """Gets a list of all modules in the project. Returns: A list of all modules. """ return self.modules.values() def module_iter(self): """Iterates over all modules in the project.""" for module_path in self.modules: yield self.modules[module_path] def resolve_rule(self, rule_path, requesting_module=None): """Gets a rule by path, supporting module lookup and dynamic loading. Args: rule_path: Path of the rule to find. Must include a semicolon. requesting_module: The module that is requesting the given rule. If not provided then no local rule paths (':foo') or relative paths are allowed. Returns: The rule with the given name or None if it was not found. Raises: NameError: The given rule name was not valid. KeyError: The given rule was not found. IOError: Unable to load referenced module. """ if not anvil.util.is_rule_path(rule_path): raise NameError('The rule path "%s" is missing a semicolon' % (rule_path)) (module_path, rule_name) = string.rsplit(rule_path, ':', 1) if self.module_resolver.can_resolve_local: if not len(module_path) and not requesting_module: module_path = '.' if not len(module_path) and not requesting_module: raise KeyError('Local rule "%s" given when no resolver defined' % ( rule_path)) module = requesting_module if len(module_path): requesting_path = None if requesting_module: requesting_path = os.path.dirname(requesting_module.path) full_path = self.module_resolver.resolve_module_path( module_path, requesting_path) module = self.modules.get(full_path, None) if not module: # Module not yet loaded - need to grab it module = self.module_resolver.load_module( full_path, self.rule_namespace) if module: self.add_module(module) else: raise IOError('Module "%s" not found', module_path) return module.get_rule(rule_name)
class ModuleLoader(object): """A utility type that handles loading modules from files. A loader should only be used to load a single module and then be discarded. """ def __init__(self, path, rule_namespace=None, modes=None): """Initializes a loader. Args: path: File-system path to the module. rule_namespace: Rule namespace to use for rule definitions. """ self.path = path self.rule_namespace = rule_namespace if not self.rule_namespace: self.rule_namespace = RuleNamespace() self.rule_namespace.discover() self.modes = {} if modes: for mode in modes: if self.modes.has_key(mode): raise KeyError('Duplicate mode "%s" defined' % (mode)) self.modes[mode] = True self.code_str = None self.code_ast = None self.code_obj = None self._current_scope = None def load(self, source_string=None): """Loads the module from the given path and prepares it for execution. Args: source_string: A string to use as the source. If not provided the file will be loaded at the initialized path. Raises: IOError: The file could not be loaded or read. SyntaxError: An error occurred parsing the module. """ if self.code_str: raise Exception('ModuleLoader load called multiple times') # Read the source as a string if source_string is None: try: with io.open(self.path, 'r') as f: self.code_str = f.read() except Exception as e: raise IOError('Unable to find or read %s' % (self.path)) else: self.code_str = source_string # Parse the AST # This will raise errors if it is not valid self.code_ast = ast.parse(self.code_str, self.path, 'exec') # Compile self.code_obj = compile(self.code_ast, self.path, 'exec') def execute(self): """Executes the module and returns a Module instance. Returns: A new Module instance with all of the rules. Raises: NameError: A function or variable name was not found. """ all_rules = None anvil.rule.begin_capturing_emitted_rules() try: # Setup scope scope = {} self._current_scope = scope self.rule_namespace.populate_scope(scope) self._add_builtins(scope) # Execute! exec self.code_obj in scope finally: self._current_scope = None all_rules = anvil.rule.end_capturing_emitted_rules() # Gather rules and build the module module = Module(self.path) module.add_rules(all_rules) return module def _add_builtins(self, scope): """Adds builtin functions and types to a scope. Args: scope: Scope dictionary. """ scope['glob'] = self.glob scope['include_rules'] = self.include_rules scope['select_one'] = self.select_one scope['select_any'] = self.select_any scope['select_many'] = self.select_many def glob(self, expr): """Globs the given expression with the base path of the module. This uses the glob2 module and supports recursive globs ('**/*'). Args: expr: Glob expression. Returns: A list of all files that match the glob expression. """ if not expr or not len(expr): return [] base_path = os.path.dirname(self.path) glob_path = os.path.join(base_path, expr) return list(glob2.iglob(glob_path)) def include_rules(self, srcs): """Scans the given paths for rules to include. Source strings must currently be file paths. Future versions may support referencing other rules. Args: srcs: A list of source strings or a single source string. """ base_path = os.path.dirname(self.path) if isinstance(srcs, str): srcs = [srcs] for src in srcs: # TODO(benvanik): support references - requires making the module loader # reentrant so that the referenced module can be loaded inline src = os.path.normpath(os.path.join(base_path, src)) self.rule_namespace.discover_in_file(src) # Repopulate the scope so future statements pick up the new rules self.rule_namespace.populate_scope(self._current_scope) def select_one(self, d, default_value): """Selects a single value from the given tuple list based on the current mode settings. This is similar to select_any, only it ensures a reliable ordering in the case of multiple modes being matched. If 'A' and 'B' are two non-exclusive modes, then pass [('A', ...), ('B', ...)] to ensure ordering. If only A or B is defined then the respective values will be selected, and if both are defined then the last matching tuple will be returned - in the case of both A and B being defined, the value of 'B'. Args: d: A list of (key, value) tuples. default_value: The value to return if nothing matches. Returns: A value from the given dictionary based on the current mode, and if none match default_value. Raises: KeyError: Multiple keys were matched in the given dictionary. """ value = None any_match = False for mode_tuple in d: if self.modes.has_key(mode_tuple[0]): any_match = True value = mode_tuple[1] if not any_match: return default_value return value def select_any(self, d, default_value): """Selects a single value from the given dictionary based on the current mode settings. If multiple keys match modes, then a random value will be returned. If you want to ensure consistent return behavior prefer select_one. This is only useful for exclusive modes (such as 'RELEASE' and 'DEBUG'). For example, if 'DEBUG' and 'RELEASE' are exclusive modes, one can use a dictionary that has 'DEBUG' and 'RELEASE' as keys and if both DEBUG and RELEASE are defined as modes then a KeyError will be raised. Args: d: Dictionary of mode key-value pairs. default_value: The value to return if nothing matches. Returns: A value from the given dictionary based on the current mode, and if none match default_value. Raises: KeyError: Multiple keys were matched in the given dictionary. """ value = None any_match = False for mode in d: if self.modes.has_key(mode): if any_match: raise KeyError( 'Multiple modes match in the given dictionary - use select_one ' 'instead to ensure ordering') any_match = True value = d[mode] if not any_match: return default_value return value def select_many(self, d, default_value): """Selects as many values from the given dictionary as match the current mode settings. This expects the values of the keys in the dictionary to be uniform (for example, all lists, dictionaries, or primitives). If any do not match a TypeError is thrown. If values are dictionaries then the result will be a dictionary that is an aggregate of all matching values. If the values are lists then a single combined list is returned. All other types are placed into a list that is returned. Args: d: Dictionary of mode key-value pairs. default_value: The value to return if nothing matches. Returns: A list or dictionary of combined values that match any modes, or the default_value. Raises: TypeError: The type of a value does not match the expected type. """ if isinstance(default_value, list): results = [] elif isinstance(default_value, dict): results = {} else: results = [] any_match = False for mode in d: if self.modes.has_key(mode): any_match = True mode_value = d[mode] if isinstance(mode_value, list): if type(mode_value) != type(default_value): raise TypeError('Type mismatch in dictionary (expected list)') results.extend(mode_value) elif isinstance(mode_value, dict): if type(mode_value) != type(default_value): raise TypeError('Type mismatch in dictionary (expected dict)') results.update(mode_value) else: if type(default_value) == list: raise TypeError('Type mismatch in dictionary (expected list)') elif type(default_value) == dict: raise TypeError('Type mismatch in dictionary (expected dict)') results.append(mode_value) if not any_match: if default_value is None: return None elif isinstance(default_value, list): results.extend(default_value) elif isinstance(default_value, dict): results.update(default_value) else: results.append(default_value) return results