Example #1
0
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
Example #2
0
 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
Example #3
0
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
Example #4
0
 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
Example #5
0
    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,
            )