Example #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)
Example #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
Example #3
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)
Example #4
0
def test_flag_format_env_variable():
    """Test flag formatting and env variable."""
    class OtherFlags(Enum):
        """Some flags to be used on the assertions below."""

        MULTI_WORD = 1
        SOME_OPTION = 2

    assert NitpickApp.format_flag(
        OtherFlags.MULTI_WORD) == "--nitpick-multi-word"
    os.environ["NITPICK_SOME_OPTION"] = "something"
    assert NitpickApp.format_env(
        OtherFlags.SOME_OPTION) == "NITPICK_SOME_OPTION"
    assert NitpickApp.get_env(OtherFlags.SOME_OPTION) == "something"
    assert NitpickApp.get_env(OtherFlags.MULTI_WORD) == ""
Example #5
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
Example #6
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", {})
Example #7
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)
Example #8
0
 def add_options(option_manager: OptionManager):
     """Add the offline option."""
     option_manager.add_option(
         NitpickApp.format_flag(NitpickApp.Flags.OFFLINE),
         action="store_true",
         # dest="offline",
         help=NitpickApp.Flags.OFFLINE.value,
     )
Example #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)
            )
Example #10
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
Example #11
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)
Example #12
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 []
Example #13
0
    def flake8(self, offline=False, file_index: int = 0) -> "ProjectMock":
        """Simulate a manual flake8 run.

        - Recreate the global app.
        - Change the working dir to the mocked project root.
        - Lint one of the project files. If no index is provided, use the default file that's always created.
        """
        os.chdir(str(self.root_dir))
        NitpickApp.create_app(offline)

        npc = NitpickExtension(filename=str(self.files_to_lint[file_index]))
        self._original_errors = list(npc.run())

        self._errors = set()
        for flake8_error in self._original_errors:
            line, col, message, class_ = flake8_error
            if not (line == 0 and col == 0 and message.startswith(ERROR_PREFIX)
                    and class_ is NitpickExtension):
                raise AssertionError()
            self._errors.add(message)
        return self
Example #14
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()
Example #15
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
Example #16
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)
Example #17
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
Example #18
0
    def flake8_error(self,
                     number: int,
                     message: str,
                     suggestion: str = None,
                     add_to_base_number=True) -> Flake8Error:
        """Return a flake8 error as a tuple."""
        # pylint: disable=import-outside-toplevel
        from nitpick.app import NitpickApp
        from nitpick.exceptions import NitpickError

        error = NitpickError()
        error.error_base_number = self.error_base_number
        error.error_prefix = self.error_prefix
        error.number = number
        error.message = message
        if suggestion:
            error.suggestion = suggestion
        error.add_to_base_number = add_to_base_number
        return NitpickApp.as_flake8_warning(error)
Example #19
0
    "package-json.toml": "package.json_",
    "poetry.toml": "Poetry_",
    "pre-commit/bash.toml": "Bash_",
    "pre-commit/commitlint.toml": "commitlint_",
    "pre-commit/general.toml": "pre-commit_ (hooks)",
    "pre-commit/main.toml": "pre-commit_ (main)",
    "pre-commit/python.toml": "pre-commit_ (Python hooks)",
    "pylint.toml": "Pylint_",
    "pytest.toml": "pytest_",
    "python35-36-37.toml": "Python 3.5, 3.6 or 3.7",
    "python35-36-37-38.toml": "Python 3.5, 3.6, 3.7 to 3.8",
    "python36-37.toml": "Python 3.6 or 3.7",
    "python36.toml": "Python 3.6",
    "python37.toml": "Python 3.7",
})
app = NitpickApp.create_app()

divider = ".. auto-generated-from-here"
docs_dir = Path(__file__).parent.absolute()  # type: Path
styles_dir = docs_dir.parent / "styles"  # type: Path


def write_rst(rst_file: Path, blocks: List[str]):
    """Write content to the .rst file."""
    old_content = rst_file.read_text()
    cut_position = old_content.index(divider)
    new_content = old_content[:cut_position + len(divider) + 1]
    new_content += "\n".join(blocks)
    rst_file.write_text(new_content.strip() + "\n")
    click.secho("{} generated".format(rst_file), fg="green")
Example #20
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)