Exemple #1
0
    def include_multiple_styles(self, chosen_styles: StrOrList) -> None:
        """Include a list of styles (or just one) into this style tree."""
        style_uris = [chosen_styles] if isinstance(chosen_styles, str) else chosen_styles  # type: List[str]
        for style_uri in style_uris:
            style_path = self.get_style_path(style_uri)  # type: Optional[Path]
            if not style_path:
                continue

            toml = TOMLFormat(path=style_path)
            try:
                toml_dict = toml.as_data
            except TomlDecodeError as err:
                NitpickApp.current().add_style_error(style_path.name, pretty_exception(err, "Invalid TOML"))
                # If the TOML itself could not be parsed, we can't go on
                return

            try:
                display_name = str(style_path.relative_to(NitpickApp.current().root_dir))
            except ValueError:
                display_name = style_uri
            self.validate_style(display_name, toml_dict)
            self._all_styles.add(toml_dict)

            sub_styles = search_dict(NITPICK_STYLES_INCLUDE_JMEX, toml_dict, [])  # type: StrOrList
            if sub_styles:
                self.include_multiple_styles(sub_styles)
Exemple #2
0
def test_offline_flag_env_variable(tmpdir):
    """Test if the offline flag or environment variable was set."""
    with tmpdir.as_cwd():
        _call_main([])
        assert NitpickApp.current().offline is False

        _call_main(["--nitpick-offline"])
        assert NitpickApp.current().offline is True

        os.environ["NITPICK_OFFLINE"] = "1"
        _call_main([])
        assert NitpickApp.current().offline is True
Exemple #3
0
    def fetch_style_from_url(self, url: str) -> Optional[Path]:
        """Fetch a style file from a URL, saving the contents in the cache dir."""
        if NitpickApp.current().offline:
            # No style will be fetched in offline mode
            return None

        if self._first_full_path and not is_url(url):
            prefix, rest = self._first_full_path.split(":/")
            domain_plus_url = str(rest).strip("/").rstrip("/") + "/" + url
            new_url = "{}://{}".format(prefix, domain_plus_url)
        else:
            new_url = url

        parsed_url = list(urlparse(new_url))
        if not parsed_url[2].endswith(TOML_EXTENSION):
            parsed_url[2] += TOML_EXTENSION
        new_url = urlunparse(parsed_url)

        if new_url in self._already_included:
            return None

        if not NitpickApp.current().cache_dir:
            raise FileNotFoundError("Cache dir does not exist")

        try:
            response = requests.get(new_url)
        except requests.ConnectionError:
            click.secho(
                "Your network is unreachable. Fix your connection or use {} / {}=1".format(
                    NitpickApp.format_flag(NitpickApp.Flags.OFFLINE), NitpickApp.format_env(NitpickApp.Flags.OFFLINE)
                ),
                fg="red",
                err=True,
            )
            return None
        if not response.ok:
            raise FileNotFoundError("Error {} fetching style URL {}".format(response, new_url))

        # Save the first full path to be used by the next files without parent.
        if not self._first_full_path:
            self._first_full_path = new_url.rsplit("/", 1)[0]

        contents = response.text
        style_path = NitpickApp.current().cache_dir / "{}.toml".format(slugify(new_url))
        NitpickApp.current().cache_dir.mkdir(parents=True, exist_ok=True)
        style_path.write_text(contents)

        LOGGER.info("Loading style from URL %s into %s", new_url, style_path)
        self._already_included.add(new_url)

        return style_path
Exemple #4
0
    def __init__(self, config: JsonDict, file_name: str = None) -> None:
        if file_name is not None:
            self.file_name = file_name

        self.error_prefix = "File {}".format(self.file_name)
        self.file_path = NitpickApp.current(
        ).root_dir / self.file_name  # type: Path

        # Configuration for this file as a TOML dict, taken from the style file.
        self.file_dict = config or {}  # type: JsonDict

        # Nitpick configuration for this file as a TOML dict, taken from the style file.
        self.nitpick_file_dict = search_dict(
            'files."{}"'.format(self.file_name),
            NitpickApp.current().config.nitpick_section, {})  # type: JsonDict
Exemple #5
0
    def merge_styles(self) -> YieldFlake8Error:
        """Merge one or multiple style files."""
        if not self.validate_pyproject_tool_nitpick():
            # If the project is misconfigured, don't even continue.
            return

        configured_styles = self.tool_nitpick_dict.get("style",
                                                       "")  # type: StrOrList
        style = Style()
        style.find_initial_styles(configured_styles)

        self.style_dict = style.merge_toml_dict()
        if not NitpickApp.current().style_errors:
            # Don't show duplicated errors: if there are style errors already, don't validate the merged style.
            style.validate_style(MERGED_STYLE_TOML, self.style_dict)

        from nitpick.flake8 import NitpickExtension  # pylint: disable=import-outside-toplevel

        minimum_version = search_dict(NITPICK_MINIMUM_VERSION_JMEX,
                                      self.style_dict, None)
        if minimum_version and version_to_tuple(
                NitpickExtension.version) < version_to_tuple(minimum_version):
            yield self.flake8_error(
                3,
                "The style file you're using requires {}>={}".format(
                    PROJECT_NAME, minimum_version) +
                " (you have {}). Please upgrade".format(
                    NitpickExtension.version),
            )

        self.nitpick_section = self.style_dict.get("nitpick", {})
        self.nitpick_files_section = self.nitpick_section.get("files", {})
Exemple #6
0
    def check_files(self, present: bool) -> YieldFlake8Error:
        """Check files that should be present or absent."""
        key = "present" if present else "absent"
        message = "exist" if present else "be deleted"
        absent = not present
        for file_name, extra_message in NitpickApp.current(
        ).config.nitpick_files_section.get(key, {}).items():
            file_path = NitpickApp.current().root_dir / file_name  # type: Path
            exists = file_path.exists()
            if (present and exists) or (absent and not exists):
                continue

            full_message = "File {} should {}".format(file_name, message)
            if extra_message:
                full_message += ": {}".format(extra_message)

            yield self.flake8_error(3 if present else 4, full_message)
Exemple #7
0
 def validate_pyproject_tool_nitpick(self) -> bool:
     """Validate the ``pyroject.toml``'s ``[tool.nitpick]`` section against a Marshmallow schema."""
     pyproject_path = NitpickApp.current(
     ).root_dir / PyProjectTomlPlugin.file_name  # type: Path
     if pyproject_path.exists():
         self.pyproject_toml = TOMLFormat(path=pyproject_path)
         self.tool_nitpick_dict = search_dict(TOOL_NITPICK_JMEX,
                                              self.pyproject_toml.as_data,
                                              {})
         pyproject_errors = ToolNitpickSectionSchema().validate(
             self.tool_nitpick_dict)
         if pyproject_errors:
             NitpickApp.current().add_style_error(
                 PyProjectTomlPlugin.file_name,
                 "Invalid data in [{}]:".format(TOOL_NITPICK),
                 flatten_marshmallow_errors(pyproject_errors),
             )
             return False
     return True
Exemple #8
0
 def load_fixed_dynamic_classes(cls) -> None:
     """Separate classes with fixed file names from classes with dynamic files names."""
     cls.fixed_name_classes = set()
     cls.dynamic_name_classes = set()
     for plugin_class in NitpickApp.current(
     ).plugin_manager.hook.plugin_class():
         if plugin_class.file_name:
             cls.fixed_name_classes.add(plugin_class)
         else:
             cls.dynamic_name_classes.add(plugin_class)
Exemple #9
0
    def validate_style(self, style_file_name: str, original_data: JsonDict):
        """Validate a style file (TOML) against a Marshmallow schema."""
        self.rebuild_dynamic_schema(original_data)
        style_errors = self._dynamic_schema_class().validate(original_data)

        if style_errors:
            has_nitpick_jsonfile_section = style_errors.get(PROJECT_NAME, {}).pop("JSONFile", None)
            if has_nitpick_jsonfile_section:
                warnings.warn(
                    "The [nitpick.JSONFile] section is not needed anymore; just declare your JSON files directly",
                    DeprecationWarning,
                )
                if not style_errors[PROJECT_NAME]:
                    style_errors.pop(PROJECT_NAME)

        if style_errors:
            NitpickApp.current().add_style_error(
                style_file_name, "Invalid config:", flatten_marshmallow_errors(style_errors)
            )
Exemple #10
0
    def parse_options(option_manager: OptionManager, options, args):  # pylint: disable=unused-argument
        """Create the Nitpick app, set logging from the verbose flags, set offline mode.

        This function is called only once by flake8, so it's a good place to create the app.
        """
        log_mapping = {1: logging.INFO, 2: logging.DEBUG}
        logging.basicConfig(
            level=log_mapping.get(options.verbose, logging.WARNING))

        NitpickApp.create_app(
            offline=bool(options.nitpick_offline
                         or NitpickApp.get_env(NitpickApp.Flags.OFFLINE)))
        LOGGER.info("Offline mode: %s", NitpickApp.current().offline)
Exemple #11
0
    def check_exists(self) -> YieldFlake8Error:
        """Check if the file should exist."""
        config_data_exists = bool(self.file_dict or self.nitpick_file_dict)
        should_exist = NitpickApp.current().config.nitpick_files_section.get(
            TOMLFormat.group_name_for(self.file_name), True)  # type: bool
        file_exists = self.file_path.exists()

        if config_data_exists and not file_exists:
            suggestion = self.suggest_initial_contents()
            phrases = [" was not found"]
            message = NitpickApp.current().config.nitpick_files_section.get(
                self.file_name)
            if message and isinstance(message, str):
                phrases.append(message)
            if suggestion:
                phrases.append("Create it with this content:")
            yield self.flake8_error(1, ". ".join(phrases), suggestion)
        elif not should_exist and file_exists:
            # Only display this message if the style is valid.
            if not NitpickApp.current().style_errors:
                yield self.flake8_error(2, " should be deleted")
        elif file_exists and config_data_exists:
            yield from self.check_rules()
Exemple #12
0
    def find_initial_styles(self, configured_styles: StrOrList):
        """Find the initial style(s) and include them."""
        if configured_styles:
            chosen_styles = configured_styles
            log_message = "Styles configured in {}: %s".format(PyProjectTomlPlugin.file_name)
        else:
            paths = climb_directory_tree(NitpickApp.current().root_dir, [NITPICK_STYLE_TOML])
            if paths:
                chosen_styles = str(sorted(paths)[0])
                log_message = "Found style climbing the directory tree: %s"
            else:
                chosen_styles = self.get_default_style_url()
                log_message = "Loading default Nitpick style %s"
        LOGGER.info(log_message, chosen_styles)

        self.include_multiple_styles(chosen_styles)
Exemple #13
0
    def merge_toml_dict(self) -> JsonDict:
        """Merge all included styles into a TOML (actually JSON) dictionary."""
        app = NitpickApp.current()
        if not app.cache_dir:
            return {}
        merged_dict = self._all_styles.merge()
        merged_style_path = app.cache_dir / MERGED_STYLE_TOML  # type: Path
        toml = TOMLFormat(data=merged_dict)

        attempt = 1
        while attempt < 5:
            try:
                app.cache_dir.mkdir(parents=True, exist_ok=True)
                merged_style_path.write_text(toml.reformatted)
                break
            except OSError:
                attempt += 1

        return merged_dict
Exemple #14
0
    def run(self) -> YieldFlake8Error:
        """Run the check plugin."""
        has_errors = False
        app = NitpickApp.current()
        for err in app.init_errors:
            has_errors = True
            yield NitpickApp.as_flake8_warning(err)
        if has_errors:
            return []

        current_python_file = Path(self.filename)
        if current_python_file.absolute() != app.main_python_file.absolute():
            # Only report warnings once, for the main Python file of this project.
            LOGGER.debug("Ignoring file: %s", self.filename)
            return []
        LOGGER.debug("Nitpicking file: %s", self.filename)

        yield from itertools.chain(app.config.merge_styles(),
                                   self.check_files(True),
                                   self.check_files(False))

        has_errors = False
        for err in app.style_errors:
            has_errors = True
            yield NitpickApp.as_flake8_warning(err)
        if has_errors:
            return []

        # Get all root keys from the style TOML.
        for path, config_dict in app.config.style_dict.items():
            # All except "nitpick" are file names.
            if path == PROJECT_NAME:
                continue

            # For each file name, find the plugin that can handle the file.
            tags = identify.tags_from_filename(path)
            for base_file in app.plugin_manager.hook.handle_config_file(  # pylint: disable=no-member
                    config=config_dict,
                    file_name=path,
                    tags=tags):
                yield from base_file.check_exists()

        return []
Exemple #15
0
 def check_rules(self) -> YieldFlake8Error:
     """Check missing key/value pairs in pyproject.toml."""
     if NitpickApp.current().config.pyproject_toml:
         comparison = NitpickApp.current(
         ).config.pyproject_toml.compare_with_flatten(self.file_dict)
         yield from self.warn_missing_different(comparison)