def document_configuration(cls, ruleset="std"): """Add a 'Configuration' section to a Rule docstring. Utilize the the metadata in config_info to dynamically document the configuration options for a given rule. This is a little hacky, but it allows us to propagate configuration options in the docs, from a single source of truth. """ if ruleset == "std": config_info = get_config_info() else: # pragma: no cover raise ( NotImplementedError( "Add another config info dict for the new ruleset here!" ) ) config_doc = "\n **Configuration**\n" try: for keyword in sorted(cls.config_keywords): try: info_dict = config_info[keyword] except KeyError: # pragma: no cover raise KeyError( "Config value {!r} for rule {} is not configured in " "`config_info`.".format(keyword, cls.__name__) ) config_doc += "\n * ``{}``: {}".format(keyword, info_dict["definition"]) if ( config_doc[-1] != "." and config_doc[-1] != "?" and config_doc[-1] != "\n" ): config_doc += "." if "validation" in info_dict: config_doc += " Must be one of ``{}``.".format(info_dict["validation"]) except AttributeError: rules_logger.info(f"No config_keywords defined for {cls.__name__}") return cls # Add final blank line config_doc += "\n" if "**Anti-pattern**" in cls.__doc__: # Match `**Anti-pattern**`, then insert configuration before # the first occurrences pattern = re.compile("(\\s{4}\\*\\*Anti-pattern\\*\\*)", flags=re.MULTILINE) cls.__doc__ = pattern.sub(f"\n{config_doc}\n\\1", cls.__doc__, count=1) else: # Match last `\n` or `.`, then append configuration pattern = re.compile("(\\.|\\n)$", flags=re.MULTILINE) cls.__doc__ = pattern.sub(f"\\1\n{config_doc}\n", cls.__doc__, count=1) return cls
def _init_capitalisation_policy(self): """Called first time rule is evaluated to fetch & cache the policy.""" cap_policy_name = next(k for k in self.config_keywords if k.endswith("capitalisation_policy")) self.cap_policy = getattr(self, cap_policy_name) self.cap_policy_opts = [ opt for opt in get_config_info()[cap_policy_name]["validation"] if opt != "consistent" ] self.logger.debug( f"Selected '{cap_policy_name}': '{self.cap_policy}' from options " f"{self.cap_policy_opts}") cap_policy = self.cap_policy cap_policy_opts = self.cap_policy_opts return cap_policy, cap_policy_opts
def document_configuration(cls, ruleset="std"): """Add a 'Configuration' section to a Rule docstring. Utilize the the metadata in config_info to dynamically document the configuration options for a given rule. This is a little hacky, but it allows us to propagate configuration options in the docs, from a single source of truth. """ if ruleset == "std": config_info = get_config_info() else: raise ( NotImplementedError( "Add another config info dict for the new ruleset here!" ) ) config_doc = "\n | **Configuration**" try: for keyword in sorted(cls.config_keywords): try: info_dict = config_info[keyword] except KeyError: raise KeyError( "Config value {!r} for rule {} is not configured in `config_info`.".format( keyword, cls.__name__ ) ) config_doc += "\n | `{0}`: {1}.".format( keyword, info_dict["definition"] ) if "validation" in info_dict: config_doc += " Must be one of {0}.".format(info_dict["validation"]) config_doc += "\n |" except AttributeError: rules_logger.info("No config_keywords defined for {0}".format(cls.__name__)) return cls # Add final blank line config_doc += "\n" # Add the configuration section immediately after the class description # docstring by inserting after the first line break, or first period, # if there is no line break. end_of_class_description = "." if "\n" not in cls.__doc__ else "\n" cls.__doc__ = cls.__doc__.replace(end_of_class_description, "\n" + config_doc, 1) return cls
def _init_capitalisation_policy(self): """Called first time rule is evaluated to fetch & cache the policy.""" cap_policy_name = next(k for k in self.config_keywords if k.endswith("capitalisation_policy")) self.cap_policy = getattr(self, cap_policy_name) self.cap_policy_opts = [ opt for opt in get_config_info()[cap_policy_name]["validation"] if opt != "consistent" ] # Use str() as L040 uses bools which might otherwise be read as bool ignore_words_config = str(getattr(self, "ignore_words")) if ignore_words_config and ignore_words_config != "None": self.ignore_words_list = self.split_comma_separated_string( ignore_words_config.lower()) else: self.ignore_words_list = [] self.logger.debug( f"Selected '{cap_policy_name}': '{self.cap_policy}' from options " f"{self.cap_policy_opts}") cap_policy = self.cap_policy cap_policy_opts = self.cap_policy_opts ignore_words_list = self.ignore_words_list return cap_policy, cap_policy_opts, ignore_words_list
def _eval(self, segment, memory, parent_stack, **kwargs): """Inconsistent capitalisation of keywords. We use the `memory` feature here to keep track of cases known to be INconsistent with what we've seen so far as well as the top choice for what the possible case is. """ # Skip if not an element of the specified type/name if (("type", segment.type) not in self._target_elems) and ( ("name", segment.name) not in self._target_elems): return LintResult(memory=memory) # Get the capitalisation policy configuration cap_policy_name = next(k for k in self.config_keywords if k.endswith("capitalisation_policy")) cap_policy = getattr(self, cap_policy_name) cap_policy_opts = [ opt for opt in get_config_info()[cap_policy_name]["validation"] if opt != "consistent" ] self.logger.debug( f"Selected '{cap_policy_name}': '{cap_policy}' from options " f"{cap_policy_opts}") refuted_cases = memory.get("refuted_cases", set()) # Which cases are definitely inconsistent with the segment? if segment.raw[0] != segment.raw[0].upper(): refuted_cases.update(["upper", "capitalise", "pascal"]) if segment.raw != segment.raw.lower(): refuted_cases.update(["lower"]) else: refuted_cases.update(["lower"]) if segment.raw != segment.raw.upper(): refuted_cases.update(["upper"]) if segment.raw != segment.raw.capitalize(): refuted_cases.update(["capitalise"]) if not segment.raw.isalnum(): refuted_cases.update(["pascal"]) # Update the memory memory["refuted_cases"] = refuted_cases self.logger.debug( f"Refuted cases after segment '{segment.raw}': {refuted_cases}") # Skip if no inconsistencies, otherwise compute a concrete policy # to convert to. if cap_policy == "consistent": possible_cases = [ c for c in cap_policy_opts if c not in refuted_cases ] self.logger.debug( f"Possible cases after segment '{segment.raw}': {possible_cases}" ) if possible_cases: # Save the latest possible case and skip memory["latest_possible_case"] = possible_cases[0] self.logger.debug( f"Consistent capitalization, returning with memory: {memory}" ) return LintResult(memory=memory) else: concrete_policy = memory.get("latest_possible_case", "upper") self.logger.debug( f"Getting concrete policy '{concrete_policy}' from memory") else: if cap_policy not in refuted_cases: # Skip self.logger.debug( f"Consistent capitalization {cap_policy}, returning with " f"memory: {memory}") return LintResult(memory=memory) else: concrete_policy = cap_policy self.logger.debug( f"Setting concrete policy '{concrete_policy}' from cap_policy" ) # We need to change the segment to match the concrete policy if concrete_policy in ["upper", "lower", "capitalise"]: if "pascal" in cap_policy_opts and segment.raw[0].isupper(): # Insert undescores in transitions between lower and upper fixed_raw = re.sub("(?<=[a-z0-9])(?=[A-Z])", "_", segment.raw) self.logger.debug(f"Inserted underscores: {fixed_raw}") else: fixed_raw = segment.raw if concrete_policy == "upper": fixed_raw = fixed_raw.upper() elif concrete_policy == "lower": fixed_raw = fixed_raw.lower() elif concrete_policy == "capitalise": fixed_raw = fixed_raw.capitalize() elif concrete_policy == "pascal": fixed_raw = re.sub( "([^a-zA-Z0-9]+|^)([a-zA-Z0-9])([a-zA-Z0-9]*)", lambda match: match.group(2).upper() + match.group(3).lower(), segment.raw, ) if fixed_raw == segment.raw: # No need to fix self.logger.debug( f"Capitalisation of segment '{segment.raw}' already OK with policy " f"'{concrete_policy}', returning with memory {memory}") return LintResult(memory=memory) else: # Return the fixed segment self.logger.debug( f"INCONSISTENT Capitalisation of segment '{segment.raw}', fixing to " f"'{fixed_raw}' and returning with memory {memory}") return LintResult( anchor=segment, fixes=[ LintFix( "edit", segment, segment.__class__(raw=fixed_raw, pos_marker=segment.pos_marker), ) ], memory=memory, )