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: Nitpick.current_app().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(Nitpick.current_app().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)
def check_exists(self) -> YieldFlake8Error: """Check if the file should exist.""" for _ in self.multiple_files: config_data_exists = bool(self.file_dict or self.nitpick_file_dict) should_exist = Nitpick.current_app( ).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 = Nitpick.current_app( ).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 Nitpick.current_app().style_errors: yield self.flake8_error(2, " should be deleted") elif file_exists and config_data_exists: yield from self.check_rules()
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: Nitpick.current_app().add_style_error( style_file_name, "Invalid config:", flatten_marshmallow_errors(style_errors))
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)) Nitpick.create_app(offline=bool(options.nitpick_offline or Nitpick.get_env(Nitpick.Flags.OFFLINE))) LOGGER.info("Offline mode: %s", Nitpick.current_app().offline)
def test_offline_flag_env_variable(tmpdir): """Test if the offline flag or environment variable was set.""" with tmpdir.as_cwd(): _call_main([]) assert Nitpick.current_app().offline is False _call_main(["--nitpick-offline"]) assert Nitpick.current_app().offline is True os.environ["NITPICK_OFFLINE"] = "1" _call_main([]) assert Nitpick.current_app().offline is True
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 Nitpick.format_flag(OtherFlags.MULTI_WORD) == "--nitpick-multi-word" os.environ["NITPICK_SOME_OPTION"] = "something" assert Nitpick.format_env(OtherFlags.SOME_OPTION) == "NITPICK_SOME_OPTION" assert Nitpick.get_env(OtherFlags.SOME_OPTION) == "something" assert Nitpick.get_env(OtherFlags.MULTI_WORD) == ""
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 Nitpick.current_app().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.plugin import NitpickChecker # pylint: disable=import-outside-toplevel minimum_version = search_dict(NITPICK_MINIMUM_VERSION_JMEX, self.style_dict, None) if minimum_version and version_to_tuple( NitpickChecker.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( NitpickChecker.version), ) self.nitpick_section = self.style_dict.get("nitpick", {}) self.nitpick_files_section = self.nitpick_section.get("files", {})
def _set_current_data(self, file_name: str) -> None: """Set data for the current file name, either if there are multiple or single files.""" if self.has_multiple_files: self.file_name = file_name self.error_prefix = "File {}".format(self.file_name) self.file_path = Nitpick.current_app().root_dir / self.file_name # Configuration for this file as a TOML dict, taken from the style file. self.file_dict = Nitpick.current_app().config.style_dict.get( TomlFormat.group_name_for(self.file_name), {}) # 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), Nitpick.current_app().config.nitpick_section, {})
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 Nitpick.current_app().config.nitpick_files_section.get(key, {}).items(): file_path = Nitpick.current_app().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)
def add_options(option_manager: OptionManager): """Add the offline option.""" option_manager.add_option( Nitpick.format_flag(Nitpick.Flags.OFFLINE), action="store_true", # dest="offline", help=Nitpick.Flags.OFFLINE.value, )
def validate_pyproject_tool_nitpick(self) -> bool: """Validate the ``pyroject.toml``'s ``[tool.nitpick]`` section against a Marshmallow schema.""" pyproject_path = Nitpick.current_app( ).root_dir / PyProjectTomlFile.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: Nitpick.current_app().add_style_error( PyProjectTomlFile.file_name, "Invalid data in [{}]:".format(TOOL_NITPICK), flatten_marshmallow_errors(pyproject_errors), ) return False return True
def __init__(self) -> None: if self.has_multiple_files: key = "{}.file_names".format(self.__class__.__name__) self._multiple_files = search_dict( key, Nitpick.current_app().config.nitpick_section, []) # type: List[str] else: self._multiple_files = [self.file_name] self._set_current_data(self.file_name)
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)) Nitpick.create_app(offline) npc = NitpickChecker(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 NitpickChecker): raise AssertionError() self._errors.add(message) return self
def run(self) -> YieldFlake8Error: """Run the check plugin.""" has_errors = False for err in Nitpick.current_app().init_errors: has_errors = True yield Nitpick.as_flake8_warning(err) if has_errors: return [] current_python_file = Path(self.filename) if current_python_file.absolute() != Nitpick.current_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( Nitpick.current_app().config.merge_styles(), self.check_files(True), self.check_files(False) ) has_errors = False for err in Nitpick.current_app().style_errors: has_errors = True yield Nitpick.as_flake8_warning(err) if has_errors: return [] for checker_class in get_subclasses(BaseFile): checker = checker_class() yield from checker.check_exists() return []
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 Nitpick.current_app().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 Nitpick.current_app().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(Nitpick.format_flag(Nitpick.Flags.OFFLINE), Nitpick.format_env(Nitpick.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 = Nitpick.current_app().cache_dir / "{}.toml".format( slugify(new_url)) Nitpick.current_app().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
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( PyProjectTomlFile.file_name) else: paths = climb_directory_tree(Nitpick.current_app().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)
def merge_toml_dict(self) -> JsonDict: """Merge all included styles into a TOML (actually JSON) dictionary.""" app = Nitpick.current_app() 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
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 Nitpick 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 Nitpick.as_flake8_warning(error)
def check_rules(self) -> YieldFlake8Error: """Check missing key/value pairs in pyproject.toml.""" if Nitpick.current_app().config.pyproject_toml: comparison = Nitpick.current_app( ).config.pyproject_toml.compare_with_flatten(self.file_dict) yield from self.warn_missing_different(comparison)
"""Main module.""" from nitpick.app import Nitpick __version__ = "0.21.2" Nitpick.create_app()