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)
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
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
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
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", {})
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)
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
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)
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) )
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)
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()
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)
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
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 []
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)