def parse(self, argv): # Prepend with 'default' if necessary self.renv.lifecycle.mark(Lifecycle.CMD_PARSE, Lifecycle.ORDER_BEFORE, argv) feature = argv[0] if feature not in self.features: raise StopException(StopException.EFTR, "Feature '{}' is not enabled".format(feature)) argv = argv[1:] if argv: actions = self.features[feature].actions action = argv[0] if not actions or action in actions.get_names(): argv = [feature] + argv else: argv = [feature, 'default'] + argv else: argv = [feature, 'default'] self.renv.lifecycle.mark(Lifecycle.CMD_PARSE, Lifecycle.ORDER_AFTER) try: opts = vars(self.parser.parse_args(argv)) except SystemExit: raise StopException(StopException.EPAR, "Parsing failed") # Normalize project_directory opts['project_directory'] = os.path.abspath(opts['project_directory']) self.renv.update_cli_properties(opts) return opts
def action_remove(self): renv = self.renv test = Test(renv.get_prop(OPT_TEST)) if test not in self.get_tests(): raise StopException(StopException.EFS, "'{}' does not exist".format(test.name)) if not prompter.yesno("Do you really want remove this test?"): raise StopException("Nothing was removed") # Remove test folder shutil.rmtree( os.path.join(self.get_test_src_dir(), os.path.sep.join(test.path))) # Remove empty folders if any path = test.path[:-1] while path: fullpath = os.path.join(self.get_test_src_dir(), os.path.sep.join(path)) if len(os.listdir(fullpath)) == 1: shutil.rmtree(fullpath) path.pop() print("Test '{}' was removed".format(test.name))
def assert_dependency(self, name, dependency): if name == dependency: raise StopException( StopException.EPERM, "You cannot add self-dependency for '{}'".format(name)) if self.is_category(dependency): self.assert_category_has_defaults(dependency) elif not self.is_feature(dependency): raise StopException( StopException.EFTR, "Unknown dependency name '{}'".format(dependency))
def assert_category_has_defaults(self, name): cat_desc = self.categories[name] if not cat_desc.defaults: raise StopException( StopException.EFTR, str("You cannot add dependency on category '{}' because it " + "does not have default features defined").format(name))
def get_activation_order(self, request): """ Derive feature activation order from the dependency graph. :param request: `iterable` of feature and categories names :returns: total activation order. """ if request == 'default': request = sum([d.defaults for d in self.categories.values()], []) errors = list(self.get_name_errors(request)) if errors: raise StopException( StopException.EFTR, "Names {} are not features nor categories".format(errors)) flatten = self.flatten_with_defaults(request) closure = self.get_features_dependency_closure(flatten) total_order = self.get_topological_order(closure) total_order = self.clean_up_activation_order(total_order) flatten_order = [f for f in total_order if f in flatten] return total_order, flatten_order
def register_feature_category_class( self, cat_name, cat_class=FeatureCategory, features=None, defaults=None, \ requires=None, mono=True): """ Register feature category. Every name mentioned in its configuration must be previously registered to prevent potential circular dependencies. :param cat_name: unique category name :param cat_class: category's constructor :param features: `list` of feature names this category contains. Feature must be previous registered. These feature names must be previously registered. :param defaults: `list` of default features this category provides if its name mentioned as a dependency if none of its features is already active. :param requires: `list` of features and/or categories this category depends upon. These names must be previously registered. :param mono: If `True` this category can have only one active feature at a time, otherwise it can have many. """ if not features: raise StopException( StopException.EFTR, "You need to define features list for '{}'".format(cat_name)) self.categories[cat_name] = CategoryDesc(cat_name, cat_class, features, defaults, requires, mono) self.dep_graph.add_node(cat_name) for ftr_name in features: self.assert_dependency(cat_name, ftr_name) if self.feature_to_category[ftr_name] != self.sink_category_name: raise StopException( StopException.EPERM, "A feature '{}' cannot be attached to multiple categoires". format(ftr_name)) if requires: for dep_name in requires: self.assert_dependency(cat_name, dep_name) self.assert_dependency(ftr_name, dep_name) if not self.dep_graph.has_edge(dep_name, ftr_name): self.dep_graph.add_edge(dep_name, ftr_name) self.feature_to_category[ftr_name] = cat_name self.sink_category.features.remove(ftr_name)
def config_flush(self): self.lifecycle.mark(Lifecycle.CONFIG_FLUSH, Lifecycle.ORDER_BEFORE) crutch_config = self.get_crutch_config() if not crutch_config: raise StopException(StopException.ECFG, 'Crutch config is not set') self.props.update_config(crutch_config) self.props.config_flush() self.lifecycle.mark(Lifecycle.CONFIG_FLUSH, Lifecycle.ORDER_AFTER)
def check_version(self): config_version = self.renv.props.config.get('crutch_version') config_version_parts = config_version.split('.') this_version = self.renv.props.defaults.get('crutch_version') this_version_parts = this_version.split('.') # Major version mismatch is a no go if config_version_parts[0] != this_version_parts[0]: raise StopException( StopException.EVER, 'Major versions are not compatible: project({}) vs crutch({})'\ .format(config_version, this_version)) # Minor version of the project cannot be bigger than CRUTCH's if config_version_parts[1] > this_version_parts[1]: raise StopException( StopException.EVER, 'Minor versions are not compatible: project({}) vs crutch({})'\ .format(config_version, this_version))
def action_remove(self): renv = self.renv project_name = renv.get_project_name() project_directory = renv.get_project_directory() group = FileGroup(renv.get_prop(OPT_GROUP), project_directory, project_name) if not group.exists(): raise StopException( StopException.EFS, "File group '{}' does not exist".format(group.name)) if not prompter.yesno("Are you sure?"): raise StopException("Nothing was removed") print("Removed: ") for deleted in group.delete(): print("{}".format(deleted))
def action_remove(self): project_features = self.renv.get_project_features() names = self.renv.get_prop(OPT_FEATURES) _, flatten_order = self.renv.feature_ctrl.get_deactivation_order(names) if not flatten_order: raise StopException("There is nothing to remove") if not prompter.yesno("Do you really want to remove {}".format(names)): raise StopException("Nothing was removed") _, flatten_order = self.renv.feature_ctrl.deactivate_features( names, tear_down=True, skip=set(project_features) - set(flatten_order)) self.renv.set_prop('project_features', flatten_order, mirror_to_config=True) print("Removed {}".format(flatten_order))
def action_add(self): renv = self.renv test = Test(renv.get_prop(OPT_TEST)) for tst in self.get_tests(): if test.name == tst.name: raise StopException(StopException.EFS, "'{}' already exists".format(test.name)) if test.name in tst.name: raise StopException( StopException.EFS, "'{}' is a group of tests".format(test.name)) renv.set_prop(OPT_TEST, test.target, mirror_to_repl=True) jdir = os.path.join(renv.get_project_type(), 'other', self.name) jdir_test_group = os.path.join(jdir, 'group') jdir_test = os.path.join(jdir, 'test') psub = {'ProjectNameRepl': renv.get_project_name()} # Init all folders along test path fullpath = self.get_test_src_dir() path = list(reversed(test.path)) final = path[0] path = path[1:] while path: fullpath = os.path.join(fullpath, path.pop()) # If this folder already exists we must change nothing if os.path.exists(fullpath): continue self.jinja_ftr.copy_folder(jdir_test_group, fullpath, psub) fullpath = os.path.join(fullpath, final) self.jinja_ftr.copy_folder(jdir_test, fullpath, psub)
def get_deactivation_order(self, request, skip=None): """ Derive feature deactivation order from the dependency graph. :param request: `iterable` of feature and categories names :returns: total deactivation order. """ if request == 'all': request = self.categories.keys() errors = list(self.get_name_errors(request)) if errors: raise StopException( StopException.EFTR, "Names {} are not features nor categories".format(errors)) flatten = self.flatten_with_active(request) closure = set(flatten) while True: new = closure | set(self.get_features_dependency_closure(closure)) if closure == new: break closure = new if skip: for name in skip: if name in closure: closure.remove(name) # Remove implicit dependencies if some other feature that we won't remove # depends on it for implicit in [f for f in closure if f not in flatten]: for ftr_dep in self.dep_graph.successors(implicit): if ftr_dep not in closure: cat_name = self.feature_to_category[ftr_dep] cat_inst = self.active_categories.get(cat_name, None) if cat_inst and cat_inst.is_active_feature(ftr_dep): closure.remove(implicit) break total_order = self.get_reversed_topological_order(closure) total_order = self.clean_up_deactivation_order(total_order) flatten_order = [f for f in total_order if f in flatten] return total_order, flatten_order
def handle_new(self): self.renv.set_prop('project_features', ['new']) runner = self.renv.create_runner('new') runner.activate_features() self.renv.del_prop('project_features') self.renv.menu.parse(self.renv.get_prop('crutch_argv')) self.set_default_props() if os.path.exists(self.renv.get_crutch_config()): raise StopException( StopException.EPERM, 'You cannot invoke `new` on already existing CRUTCH directory {}' .format(self.renv.get_project_directory())) return runner
def action_add(self): renv = self.renv project_name = renv.get_project_name() project_directory = renv.get_project_directory() group = FileGroup(renv.get_prop(OPT_GROUP), project_directory, project_name) if group.exists(): raise StopException( StopException.EFS, "File group '{}' already exists".format(group.name)) jdir = os.path.join(renv.get_project_type(), 'other', NAME) psub = {'FileGroupRepl': os.path.join(project_name, *group.path)} renv.set_prop(OPT_GROUP, group.name, mirror_to_repl=True) self.jinja_ftr.copy_folder(jdir, project_directory, psub)
def check_crutch_config(self): if not os.path.exists(self.renv.get_prop('crutch_config')): raise StopException(StopException.EPERM, 'CRUTCH config does not exist')
def handle_prompt(self): self.set_default_props(os.path.abspath('.')) crutch_config = self.renv.get_prop('crutch_config') runner = None # Since prompt syntax highlight depends on active features we need to read # the config first and activate all the features if os.path.exists(crutch_config): self.renv.config_load() self.check_version() self.set_default_props() runner = self.renv.create_runner(self.renv.get_project_type()) runner.activate_features() # Otherwise we allow only new to run, and it will update runner upon success else: self.renv.create_runner('new').activate_features() print("CRUTCH {}".format(self.get_version())) print("Home: https://github.com/m4yers/crutch") self.renv.prompt.initialize() reinitialize_prompt = False while True: try: argv = self.renv.prompt.activate(reinitialize_prompt) reinitialize_prompt = False if not argv: continue elif argv[0] == 'new': if os.path.exists(crutch_config): raise StopException( StopException.EPERM, 'You cannot invoke `new` on already existing CRUTCH directory' ) self.renv.menu.parse(argv) self.set_default_props() runner = self.renv.feature_ctrl.get_active_feature( 'new').create() reinitialize_prompt = True else: self.renv.menu.parse(argv) runner.run() self.renv.config_flush() except StopException as stop: if stop.terminate: raise if stop.message: print(stop.message) continue except KeyboardInterrupt: break except EOFError: break raise StopException()
def activate_features(self, request, set_up=False): self.renv.lifecycle.mark_before(Lifecycle.FEATURE_CREATION, request) total_order, flatten_order = self.get_activation_order(request) LOGGER.info("Request: '%s'", request) LOGGER.info("Total order: '%s'", total_order) LOGGER.info("Flatten order: '%s'", flatten_order) # Check for conflicts within flatten(user requested) order conflicts = list() for category, features in self.get_mono_conflicts(flatten_order): conflicts.append( str("Mono category '{}' cannot have all of '{}' features " + "activated at the same time").format(category, features)) if conflicts: raise StopException( StopException.EFTR, 'There were some conflicting dependencies:\n' + '\n'.join(conflicts)) # Check for total order conflicts relative to already active categories conflicts = list() for category, feature in self.get_activation_conflicts(total_order): # In case the category was requested explicitly we can ignore this # conflict, since in this case user requested any feature(default or # already enabled) of that category if category in request: conflicts.append( "Category '{}' is already active".format(category)) # Inability to instantiate an explicit request is an error if feature in request: conflicts.append( str("Cannot activate '{}' feature because its category '{}' is " + "mono and already contains active features").format( feature, category)) if conflicts: raise StopException( StopException.EPERM, 'There were some conflicting dependencies:\n' + '\n'.join(conflicts)) self.features_in_activation_process = list(total_order) # Finally instantiate, activate and set up if needed all the features for ftr_name in total_order: cat_name = self.feature_to_category[ftr_name] cat_desc = self.categories[cat_name] cat_inst = self.active_categories.get(cat_name, None) if not cat_inst: cat_inst = cat_desc.init( self.renv, {n: self.features[n].init for n in cat_desc.features}) if set_up: self.renv.lifecycle.mark_before(Lifecycle.CATEGORY_SET_UP, cat_name) cat_inst.set_up() self.renv.lifecycle.mark_after(Lifecycle.CATEGORY_SET_UP, cat_name) self.renv.lifecycle.mark_before(Lifecycle.CATEGORY_ACTIVATE, cat_name) cat_inst.activate() self.renv.lifecycle.mark_after(Lifecycle.CATEGORY_ACTIVATE, cat_name) self.active_categories[cat_name] = cat_inst if not cat_inst.is_active_feature(ftr_name): cat_inst.activate_feature(ftr_name, set_up=set_up) self.features_in_activation_process = None self.renv.lifecycle.mark_after(Lifecycle.FEATURE_CREATION, total_order) return total_order, flatten_order
def deactivate_features(self, request, tear_down=False, skip=None): """ Deactivate features and/or categories. :param request: `list` of categories and/or features to deactivate; or a `str` equal to `all` which means deactivate all active features and categories. :param tear_down: If `True` every deactivated feature/category will be torn down, meaning it will be completely removed from the project. :param skip: `list` of feature names to skip during deactivation. Since this control has no knowledge of actual project features(it only manages dependencies, explicit and implicit), to remove a feature with its dependencies you want to skip those features that are explicitly enabled by user; otherwise the whole dependency tree will be removed unless some other feature depends on some of its sub-trees. """ self.renv.lifecycle.mark_before(Lifecycle.FEATURE_DESTRUCTION, request) total_order, flatten_order = self.get_deactivation_order(request, skip) LOGGER.info("Request: '%s'", request) LOGGER.info("Total order: '%s'", total_order) LOGGER.info("Flatten order: '%s'", flatten_order) LOGGER.info("Skip: '%s'", skip) # Before we remove anything we verify if we can do that without breaking # any dependencies conflicts = list() for ftr_name, ftr_dep in self.get_deactivation_conflicts(total_order): conflicts.append( str("Cannot deactivate feature '{}' because it is a direct " + "dependency of '{}'").format(ftr_name, ftr_dep)) if conflicts: raise StopException( StopException.EPERM, 'There were some conflicting dependencies:\n' + '\n'.join(conflicts)) for ftr_name in total_order: cat_name = self.feature_to_category[ftr_name] cat_inst = self.active_categories.get(cat_name, None) if cat_inst and cat_inst.is_active_feature(ftr_name): cat_inst.deactivate_feature(ftr_name, tear_down) if cat_inst.get_active_features(): continue self.renv.lifecycle.mark_before(Lifecycle.CATEGORY_DEACTIVATE, cat_name) cat_inst.deactivate() self.renv.lifecycle.mark_after(Lifecycle.CATEGORY_DEACTIVATE, cat_name) if tear_down: self.renv.lifecycle.mark_before( Lifecycle.CATEGORY_TEAR_DOWN, cat_name) cat_inst.tear_down() self.renv.lifecycle.mark_after( Lifecycle.CATEGORY_TEAR_DOWN, cat_name) del self.active_categories[cat_name] self.renv.lifecycle.mark_after(Lifecycle.FEATURE_DESTRUCTION, total_order) return total_order, flatten_order