PK!C++flake8_nitpick/__init__.py"""Main module.""" __version__ = "0.10.3" PK!flake8_nitpick/config.py"""Configuration of the plugin.""" import itertools import logging from pathlib import Path from shutil import rmtree from typing import Any, Dict, MutableMapping, Optional import toml from flake8_nitpick.constants import ( LOG_ROOT, NITPICK_MINIMUM_VERSION_JMEX, PROJECT_NAME, ROOT_FILES, ROOT_PYTHON_FILES, TOOL_NITPICK_JMEX, ) from flake8_nitpick.files.pyproject_toml import PyProjectTomlFile from flake8_nitpick.files.setup_cfg import SetupCfgFile from flake8_nitpick.generic import climb_directory_tree, rmdir_if_empty, search_dict, version_to_tuple from flake8_nitpick.mixin import NitpickMixin from flake8_nitpick.style import Style from flake8_nitpick.types import JsonDict, PathOrStr, StrOrList, YieldFlake8Error LOGGER = logging.getLogger(f"{LOG_ROOT}.config") class NitpickConfig(NitpickMixin): """Plugin configuration, read from the project config.""" error_base_number = 200 _singleton_instance: Optional["NitpickConfig"] = None def __init__(self) -> None: """Init instance.""" self.root_dir: Optional[Path] = None self.cache_dir: Optional[Path] = None self.main_python_file: Optional[Path] = None self.pyproject_dict: MutableMapping[str, Any] = {} self.tool_nitpick_dict: Dict[str, Any] = {} self.style_dict: MutableMapping[str, Any] = {} self.nitpick_dict: MutableMapping[str, Any] = {} self.files: Dict[str, Any] = {} @classmethod def get_singleton(cls) -> "NitpickConfig": """Init the global singleton instance of the plugin configuration, needed by all file checkers.""" if cls._singleton_instance is None: cls._singleton_instance = cls() return cls._singleton_instance @classmethod def reset_singleton(cls): """Reset the singleton instance. Useful on automated tests, to simulate ``flake8`` execution.""" cls._singleton_instance = None def find_root_dir(self, starting_file: PathOrStr) -> bool: """Find the root dir of the Python project: the dir that has one of the `ROOT_FILES`. Also clear the cache dir the first time the root dir is found. """ if self.root_dir: return True found_files = climb_directory_tree( starting_file, ROOT_FILES + (PyProjectTomlFile.file_name, SetupCfgFile.file_name) ) if not found_files: LOGGER.error("No files found while climbing directory tree from %s", str(starting_file)) return False self.root_dir = found_files[0].parent self.clear_cache_dir() return True def find_main_python_file(self) -> bool: """Find the main Python file in the root dir, the one that will be used to report Flake8 warnings.""" if not self.root_dir: return False for the_file in itertools.chain( [self.root_dir / root_file for root_file in ROOT_PYTHON_FILES], self.root_dir.glob("*.py") ): if the_file.exists(): self.main_python_file = Path(the_file) LOGGER.info("Found the file %s", the_file) return True return False def clear_cache_dir(self) -> None: """Clear the cache directory (on the project root or on the current directory).""" if not self.root_dir: return cache_root: Path = self.root_dir / ".cache" self.cache_dir = cache_root / PROJECT_NAME rmtree(str(self.cache_dir), ignore_errors=True) rmdir_if_empty(cache_root) def merge_styles(self) -> YieldFlake8Error: """Merge one or multiple style files.""" pyproject_path: Path = self.root_dir / PyProjectTomlFile.file_name if pyproject_path.exists(): self.pyproject_dict: JsonDict = toml.load(str(pyproject_path)) self.tool_nitpick_dict: JsonDict = search_dict(TOOL_NITPICK_JMEX, self.pyproject_dict, {}) configured_styles: StrOrList = self.tool_nitpick_dict.get("style", "") style = Style(self) style.find_initial_styles(configured_styles) self.style_dict = style.merge_toml_dict() from flake8_nitpick.plugin import NitpickChecker 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, f"The style file you're using requires {PROJECT_NAME}>={minimum_version}" + f" (you have {NitpickChecker.version}). Please upgrade", ) self.nitpick_dict: JsonDict = self.style_dict.get("nitpick", {}) self.files = self.nitpick_dict.get("files", {}) PK! Uflake8_nitpick/constants.py"""Constants.""" import jmespath PROJECT_NAME = "flake8-nitpick" ERROR_PREFIX = "NIP" LOG_ROOT = PROJECT_NAME.replace("-", ".") TOML_EXTENSION = ".toml" NITPICK_STYLE_TOML = f"nitpick-style{TOML_EXTENSION}" DEFAULT_NITPICK_STYLE_URL = f"https://raw.githubusercontent.com/andreoliwa/flake8-nitpick/master/{NITPICK_STYLE_TOML}" ROOT_PYTHON_FILES = ("setup.py", "manage.py", "autoapp.py") ROOT_FILES = ("requirements*.txt", "Pipfile") + ROOT_PYTHON_FILES #: Special unique separator for :py:meth:`flatten()` and :py:meth:`unflatten()`, # to avoid collision with existing key values (e.g. the default dot separator "." can be part of a pyproject.toml key). UNIQUE_SEPARATOR = "$#@" # JMESPath expressions TOOL_NITPICK_JMEX = jmespath.compile("tool.nitpick") NITPICK_STYLES_INCLUDE_JMEX = jmespath.compile("nitpick.styles.include") NITPICK_MINIMUM_VERSION_JMEX = jmespath.compile("nitpick.minimum_version") PK! flake8_nitpick/files/__init__.py"""Files that are checked by Nitpick.""" # TODO: load all modules under files/*, so get_subclasses() detects them. import flake8_nitpick.files.pre_commit # noqa PK!EaUZ Z flake8_nitpick/files/base.py"""Base class for file checkers.""" import abc from pathlib import Path from flake8_nitpick.generic import search_dict from flake8_nitpick.mixin import NitpickMixin from flake8_nitpick.types import JsonDict, YieldFlake8Error class BaseFile(NitpickMixin, metaclass=abc.ABCMeta): """Base class for file checkers.""" file_name: str error_base_number = 300 def __init__(self) -> None: """Init instance.""" from flake8_nitpick.config import NitpickConfig self.config = NitpickConfig.get_singleton() self.error_prefix = f"File {self.file_name}" self.file_path: Path = self.config.root_dir / self.file_name # Configuration for this file as a TOML dict, taken from the style file. self.file_dict: JsonDict = self.config.style_dict.get(self.toml_key(), {}) # Nitpick configuration for this file as a TOML dict, taken from the style file. self.nitpick_file_dict: JsonDict = search_dict(f'files."{self.file_name}"', self.config.nitpick_dict, {}) @classmethod def toml_key(cls): """Remove the dot in the beginning of the file name, otherwise it's an invalid TOML key.""" return cls.file_name.lstrip(".") def check_exists(self) -> YieldFlake8Error: """Check if the file should exist; if there is style configuration for the file, then it should exist. The file should exist when there is any rule configured for it in the style file, TODO: add this to the docs """ should_exist: bool = self.config.files.get(self.toml_key(), bool(self.file_dict or self.nitpick_file_dict)) file_exists = self.file_path.exists() if should_exist and not file_exists: suggestion = self.suggest_initial_contents() phrases = [" was not found"] missing_message = self.nitpick_file_dict.get("missing_message", "") if missing_message: phrases.append(missing_message) if suggestion: phrases.append(f"Create it with this content:\n{suggestion}") yield self.flake8_error(1, ". ".join(phrases)) elif not should_exist and file_exists: yield self.flake8_error(2, " should be deleted") elif file_exists: yield from self.check_rules() @abc.abstractmethod def check_rules(self) -> YieldFlake8Error: """Check rules for this file. It should be overridden by inherited class if they need.""" pass @abc.abstractmethod def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" pass PK!e3''"flake8_nitpick/files/pre_commit.py"""Checker for the `.pre-commit-config.yaml `_ file.""" from typing import Any, Dict, List, Tuple import dictdiffer import yaml from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import find_object_by_key from flake8_nitpick.types import YieldFlake8Error class PreCommitFile(BaseFile): """Checker for the `.pre-commit-config.yaml `_ file.""" file_name = ".pre-commit-config.yaml" error_base_number = 330 KEY_REPOS = "repos" KEY_HOOKS = "hooks" KEY_REPO = "repo" KEY_ID = "id" def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" suggested = self.file_dict.copy() for repo in suggested.get(self.KEY_REPOS, []): repo[self.KEY_HOOKS] = yaml.safe_load(repo[self.KEY_HOOKS]) return yaml.dump(suggested, default_flow_style=False) def check_rules(self) -> YieldFlake8Error: """Check the rules for the pre-commit hooks.""" actual = yaml.safe_load(self.file_path.open()) or {} if self.KEY_REPOS not in actual: yield self.flake8_error(1, f" doesn't have the {self.KEY_REPOS!r} root key") return actual_root = actual.copy() actual_root.pop(self.KEY_REPOS, None) expected_root = self.file_dict.copy() expected_root.pop(self.KEY_REPOS, None) for diff_type, key, values in dictdiffer.diff(expected_root, actual_root): if diff_type == dictdiffer.REMOVE: yield from self.show_missing_keys(key, values) elif diff_type == dictdiffer.CHANGE: yield from self.compare_different_keys(key, values[0], values[1]) yield from self.check_repos(actual) def check_repos(self, actual: Dict[str, Any]): """Check the repositories configured in pre-commit.""" actual_repos: List[dict] = actual[self.KEY_REPOS] or [] expected_repos: List[dict] = self.file_dict.get(self.KEY_REPOS, []) for index, expected_repo_dict in enumerate(expected_repos): repo_name = expected_repo_dict.get(self.KEY_REPO) if not repo_name: yield self.flake8_error(2, f": style file is missing {self.KEY_REPO!r} key in repo #{index}") continue actual_repo_dict = find_object_by_key(actual_repos, self.KEY_REPO, repo_name) if not actual_repo_dict: yield self.flake8_error(3, f": repo {repo_name!r} does not exist under {self.KEY_REPOS!r}") continue if self.KEY_HOOKS not in actual_repo_dict: yield self.flake8_error(4, f": missing {self.KEY_HOOKS!r} in repo {repo_name!r}") continue actual_hooks = actual_repo_dict.get(self.KEY_HOOKS) or [] yaml_expected_hooks = expected_repo_dict.get(self.KEY_HOOKS) if not yaml_expected_hooks: yield self.flake8_error(5, f": style file is missing {self.KEY_HOOKS!r} in repo {repo_name!r}") continue expected_hooks: List[dict] = yaml.safe_load(yaml_expected_hooks) for expected_dict in expected_hooks: hook_id = expected_dict.get(self.KEY_ID) if not hook_id: expected_yaml = self.format_hook(expected_dict) yield self.flake8_error(6, f": style file is missing {self.KEY_ID!r} in hook:\n{expected_yaml}") continue actual_dict = find_object_by_key(actual_hooks, self.KEY_ID, hook_id) if not actual_dict: expected_yaml = self.format_hook(expected_dict) yield self.flake8_error(7, f": missing hook with id {hook_id!r}:\n{expected_yaml}") continue def show_missing_keys(self, key, values: List[Tuple[str, Any]]): """Show the keys that are not present in a section.""" missing = dict(values) output = yaml.dump(missing, default_flow_style=False) yield self.flake8_error(8, f" has missing values:\n{output}") def compare_different_keys(self, key, raw_expected: Any, raw_actual: Any): """Compare different keys.""" if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)): # A boolean "True" or "true" might have the same effect on YAML. actual = str(raw_actual).lower() expected = str(raw_expected).lower() else: actual = raw_actual expected = raw_expected if actual != expected: example = yaml.dump({key: raw_expected}, default_flow_style=False) yield self.flake8_error(9, f": {key!r} is {raw_actual!r} but it should be like this:\n{example}") @staticmethod def format_hook(expected_dict: dict) -> str: """Format the hook so it's easy to copy and paste it to the .yaml file: ID goes first, indent with spaces.""" lines = yaml.dump(expected_dict, default_flow_style=False) output: List[str] = [] for line in lines.split("\n"): if line.startswith("id:"): output.insert(0, f" - {line}") else: output.append(f" {line}") return "\n".join(output) PK!4-&flake8_nitpick/files/pyproject_toml.py"""Checker for the `pyproject.toml `_ config file (`PEP 518 `_).""" import toml from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import flatten, unflatten from flake8_nitpick.types import YieldFlake8Error class PyProjectTomlFile(BaseFile): """Checker for the `pyproject.toml `_ config file (`PEP 518 `_).""" file_name = "pyproject.toml" error_base_number = 310 def check_rules(self) -> YieldFlake8Error: """Check missing key/value pairs in pyproject.toml.""" actual = flatten(self.config.pyproject_dict) expected = flatten(self.file_dict) if expected.items() <= actual.items(): return missing_dict = unflatten({k: v for k, v in expected.items() if k not in actual}) if missing_dict: missing_toml = toml.dumps(missing_dict) yield self.flake8_error(1, f" has missing values. Use this:\n{missing_toml}") diff_dict = unflatten({k: v for k, v in expected.items() if k in actual and expected[k] != actual[k]}) if diff_dict: diff_toml = toml.dumps(diff_dict) yield self.flake8_error(2, f" has different values. Use this:\n{diff_toml}") def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return "" PK!4!flake8_nitpick/files/setup_cfg.py"""Checker for the `setup.cfg ` config file.""" import itertools from configparser import ConfigParser from io import StringIO from typing import Any, List, Set, Tuple import dictdiffer from flake8_nitpick.files.base import BaseFile from flake8_nitpick.types import YieldFlake8Error class SetupCfgFile(BaseFile): """Checker for the `setup.cfg ` config file.""" file_name = "setup.cfg" error_base_number = 320 COMMA_SEPARATED_VALUES = "comma_separated_values" comma_separated_values: Set[str] expected_sections: Set[str] missing_sections: Set[str] def __init__(self) -> None: """Init the instance.""" super().__init__() self.comma_separated_values = set(self.nitpick_file_dict.get(self.COMMA_SEPARATED_VALUES, [])) def suggest_initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return self.get_missing_output() def get_missing_output(self, actual_sections: Set[str] = None) -> str: """Get a missing output string example from the missing sections in setup.cfg.""" self.expected_sections = set(self.file_dict.keys()) self.missing_sections = self.expected_sections - (actual_sections or set()) if self.missing_sections: missing_cfg = ConfigParser() for section in sorted(self.missing_sections): missing_cfg[section] = self.file_dict[section] return self.get_example_cfg(missing_cfg) return "" def check_rules(self) -> YieldFlake8Error: """Check missing sections and missing key/value pairs in setup.cfg.""" setup_cfg = ConfigParser() setup_cfg.read_file(self.file_path.open()) actual_sections = set(setup_cfg.sections()) missing = self.get_missing_output(actual_sections) if missing: yield self.flake8_error(1, f" has some missing sections. Use this:\n{missing}") generators = [] for section in self.expected_sections - self.missing_sections: expected_dict = self.file_dict[section] actual_dict = dict(setup_cfg[section]) for diff_type, key, values in dictdiffer.diff(expected_dict, actual_dict): if diff_type == dictdiffer.CHANGE: generators.append(self.compare_different_keys(section, key, values[0], values[1])) elif diff_type == dictdiffer.REMOVE: generators.append(self.show_missing_keys(section, key, values)) for error in itertools.chain(*generators): yield error def compare_different_keys(self, section, key, raw_expected: Any, raw_actual: Any) -> YieldFlake8Error: """Compare different keys, with special treatment when they are lists or numeric.""" combined = f"{section}.{key}" if combined in self.comma_separated_values: # The values might contain spaces actual_set = {s.strip() for s in raw_actual.split(",")} expected_set = {s.strip() for s in raw_expected.split(",")} missing = expected_set - actual_set if missing: yield self.flake8_error( 2, f" has missing values in the {key!r} key." + f" Include those values:\n[{section}]\n{key} = (...),{','.join(sorted(missing))}", ) return if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)): # A boolean "True" or "true" has the same effect on setup.cfg. actual = str(raw_actual).lower() expected = str(raw_expected).lower() else: actual = raw_actual expected = raw_expected if actual != expected: yield self.flake8_error( 3, f": [{section}]{key} is {raw_actual} but it should be like this:" + f"\n[{section}]\n{key} = {raw_expected}", ) def show_missing_keys(self, section, key, values: List[Tuple[str, Any]]) -> YieldFlake8Error: """Show the keys that are not present in a section.""" missing_cfg = ConfigParser() missing_cfg[section] = dict(values) output = self.get_example_cfg(missing_cfg) yield self.flake8_error(4, f": section [{section}] has some missing key/value pairs. Use this:\n{output}") @staticmethod def get_example_cfg(config_parser: ConfigParser) -> str: """Print an example of a config parser in a string instead of a file.""" string_stream = StringIO() config_parser.write(string_stream) output = string_stream.getvalue().strip() return output PK!y-flake8_nitpick/generic.py"""Generic functions and classes. .. testsetup:: from flake8_nitpick.generic import * """ import collections from pathlib import Path from typing import Any, Iterable, List, Optional, Tuple, Union import jmespath from jmespath.parser import ParsedResult from flake8_nitpick.types import JsonDict, PathOrStr def get_subclasses(cls): """Recursively get subclasses of a parent class.""" subclasses = [] for subclass in cls.__subclasses__(): subclasses.append(subclass) subclasses += get_subclasses(subclass) return subclasses def flatten(dict_, parent_key="", separator="."): """Flatten a nested dict. Use :py:meth:`unflatten()` to revert. >>> flatten({"root": {"sub1": 1, "sub2": {"deep": 3}}, "sibling": False}) {'root.sub1': 1, 'root.sub2.deep': 3, 'sibling': False} """ items = [] for key, value in dict_.items(): new_key = parent_key + separator + key if parent_key else key if isinstance(value, collections.abc.MutableMapping): items.extend(flatten(value, new_key, separator=separator).items()) else: items.append((new_key, value)) return dict(items) def unflatten(dict_, separator="."): """Turn back a flattened dict created by :py:meth:`flatten()` into a nested dict. >>> unflatten({"my.sub.path": True, "another.path": 3, "my.home": 4}) {'my': {'sub': {'path': True}, 'home': 4}, 'another': {'path': 3}} """ items = {} for k, v in dict_.items(): keys = k.split(separator) sub_items = items for ki in keys[:-1]: try: sub_items = sub_items[ki] except KeyError: sub_items[ki] = {} sub_items = sub_items[ki] sub_items[keys[-1]] = v return items def climb_directory_tree(starting_path: PathOrStr, file_patterns: Iterable[str]) -> Optional[List[Path]]: """Climb the directory tree looking for file patterns.""" current_dir: Path = Path(starting_path).absolute() if current_dir.is_file(): current_dir = current_dir.parent while current_dir.root != str(current_dir): for root_file in file_patterns: found_files = list(current_dir.glob(root_file)) if found_files: return found_files current_dir = current_dir.parent return None def find_object_by_key(list_: List[dict], search_key: str, search_value: Any) -> dict: """Find an object in a list, using a key/value pair to search. >>> fruits = [{"id": 1, "fruit": "banana"}, {"id": 2, "fruit": "apple"}, {"id": 3, "fruit": "mango"}] >>> find_object_by_key(fruits, "id", 1) {'id': 1, 'fruit': 'banana'} >>> find_object_by_key(fruits, "fruit", "banana") {'id': 1, 'fruit': 'banana'} >>> find_object_by_key(fruits, "fruit", "pear") {} >>> find_object_by_key(fruits, "fruit", "mango") {'id': 3, 'fruit': 'mango'} """ for obj in list_: if obj.get(search_key) == search_value: return obj return {} def rmdir_if_empty(path_or_str: PathOrStr): """Remove the directory if empty.""" path = Path(path_or_str) if not path.exists(): return try: has_items = next(path.iterdir(), False) if has_items is False: # If the directory has no more files/directories inside, try to remove the parent. path.rmdir() except FileNotFoundError: # If any removal attempt fails, just ignore it. Some other flake8 thread might have deleted the directory. pass def search_dict(jmespath_expression: Union[ParsedResult, str], data: JsonDict, default: Any) -> Any: """Search a dictionary using a JMESPath expression, and returning a default value. >>> data = {"root": {"app": [1, 2], "test": "something"}} >>> search_dict("root.app", data, None) [1, 2] >>> search_dict("root.test", data, None) 'something' >>> search_dict("root.unknown", data, "") '' >>> search_dict("root.unknown", data, None) >>> search_dict(jmespath.compile("root.app"), data, []) [1, 2] >>> search_dict(jmespath.compile("root.whatever"), data, "xxx") 'xxx' :param jmespath_expression: A compiled JMESPath expression or a string with an expression. :param data: The dictionary to be searched. :param default: Default value in case nothing is found. :return: The object that was found or the default value. """ if isinstance(jmespath_expression, str): rv = jmespath.search(jmespath_expression, data) else: rv = jmespath_expression.search(data) return rv or default def version_to_tuple(version: str = None) -> Tuple[int, ...]: """Transform a version number into a tuple of integers, for comparison. >>> version_to_tuple("") () >>> version_to_tuple(" ") () >>> version_to_tuple(None) () >>> version_to_tuple("1.0.1") (1, 0, 1) >>> version_to_tuple(" 0.2 ") (0, 2) >>> version_to_tuple(" 2 ") (2,) :param version: String with the version number. It must be integers split by dots. :return: Tuple with the version number. """ if not version: return () clean_version = version.strip() if not clean_version: return () return tuple(int(part) for part in clean_version.split(".")) def is_url(url: str) -> bool: """Return True if a string is a URL. >>> is_url("") False >>> is_url(" ") False >>> is_url("http://example.com") True """ return url.startswith("http") PK!5hhflake8_nitpick/mixin.py"""Mixin to raise flake8 errors.""" from flake8_nitpick.constants import ERROR_PREFIX from flake8_nitpick.types import Flake8Error class NitpickMixin: """Mixin to raise flake8 errors.""" error_base_number: int = 0 error_prefix: str = "" def flake8_error(self, error_number: int, error_message: str) -> Flake8Error: """Return a flake8 error as a tuple.""" final_number = self.error_base_number + error_number from flake8_nitpick.plugin import NitpickChecker return 1, 0, f"{ERROR_PREFIX}{final_number} {self.error_prefix}{error_message.rstrip()}", NitpickChecker PK!H  flake8_nitpick/plugin.py"""Flake8 plugin to check files.""" import itertools import logging from pathlib import Path import attr from flake8_nitpick import __version__ from flake8_nitpick.config import NitpickConfig from flake8_nitpick.constants import LOG_ROOT, PROJECT_NAME, ROOT_PYTHON_FILES from flake8_nitpick.files.base import BaseFile from flake8_nitpick.generic import get_subclasses from flake8_nitpick.mixin import NitpickMixin from flake8_nitpick.types import YieldFlake8Error LOGGER = logging.getLogger(f"{LOG_ROOT}.plugin") @attr.s(hash=False) class NitpickChecker(NitpickMixin): """Main plugin class.""" # Plugin config name = PROJECT_NAME version = __version__ # NitpickMixin error_base_number = 100 # Attributes config: NitpickConfig # Plugin arguments passed by Flake8 tree = attr.ib(default=None) filename = attr.ib(default="(none)") def run(self) -> YieldFlake8Error: """Run the check plugin.""" self.config = NitpickConfig().get_singleton() if not self.config.find_root_dir(self.filename): yield self.flake8_error(1, "No root dir found (is this a Python project?)") return if not self.config.find_main_python_file(): yield self.flake8_error( 2, "None of those Python files was found in the root dir" + f" {self.config.root_dir}: {', '.join(ROOT_PYTHON_FILES)}", ) return current_python_file = Path(self.filename) if current_python_file.absolute() != self.config.main_python_file.absolute(): # Only report warnings once, for the main Python file of this project. LOGGER.info("Ignoring file: %s", self.filename) return LOGGER.info("Nitpicking file: %s", self.filename) yield from itertools.chain(self.config.merge_styles(), self.check_absent_files()) for checker_class in get_subclasses(BaseFile): checker = checker_class() yield from checker.check_exists() return [] def check_absent_files(self) -> YieldFlake8Error: """Check absent files.""" for file_name, delete_message in self.config.files.get("absent", {}).items(): file_path: Path = self.config.root_dir / file_name if not file_path.exists(): continue full_message = f"File {file_name} should be deleted" if delete_message: full_message += f": {delete_message}" yield self.flake8_error(3, full_message) PK!GWhhflake8_nitpick/style.py"""Style files.""" import logging from pathlib import Path from typing import TYPE_CHECKING, List, Optional, Set from urllib.parse import urlparse, urlunparse import requests import toml from slugify import slugify from flake8_nitpick.constants import ( DEFAULT_NITPICK_STYLE_URL, LOG_ROOT, NITPICK_STYLE_TOML, NITPICK_STYLES_INCLUDE_JMEX, TOML_EXTENSION, UNIQUE_SEPARATOR, ) from flake8_nitpick.files.pyproject_toml import PyProjectTomlFile from flake8_nitpick.generic import climb_directory_tree, flatten, is_url, search_dict, unflatten from flake8_nitpick.types import JsonDict, StrOrList if TYPE_CHECKING: from flake8_nitpick.config import NitpickConfig LOGGER = logging.getLogger(f"{LOG_ROOT}.style") class Style: """Include styles recursively from one another.""" def __init__(self, config: "NitpickConfig") -> None: self.config = config self._all_flattened: JsonDict = {} self._already_included: Set[str] = set() self._first_full_path: str = "" 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 = f"Styles configured in {PyProjectTomlFile.file_name}: %s" else: paths = climb_directory_tree(self.config.root_dir, [NITPICK_STYLE_TOML]) if paths: chosen_styles = str(paths[0]) log_message = "Found style climbing the directory tree: %s" else: chosen_styles = DEFAULT_NITPICK_STYLE_URL log_message = "Loading default Nitpick style %s" LOGGER.info(log_message, chosen_styles) self.include_multiple_styles(chosen_styles) def include_multiple_styles(self, chosen_styles: StrOrList) -> None: """Include a list of styles (or just one) into this style tree.""" style_uris: List[str] = [chosen_styles] if isinstance(chosen_styles, str) else chosen_styles for style_uri in style_uris: style_path: Optional[Path] = self.get_style_path(style_uri) if not style_path: continue toml_dict = toml.load(str(style_path)) flattened_style_dict: JsonDict = flatten(toml_dict, separator=UNIQUE_SEPARATOR) self._all_flattened.update(flattened_style_dict) sub_styles: StrOrList = search_dict(NITPICK_STYLES_INCLUDE_JMEX, toml_dict, []) if sub_styles: self.include_multiple_styles(sub_styles) def get_style_path(self, style_uri: str) -> Optional[Path]: """Get the style path from the URI. Add the .toml extension if it's missing.""" clean_style_uri = style_uri.strip() style_path = None if is_url(clean_style_uri) or is_url(self._first_full_path): style_path = self.fetch_style_from_url(clean_style_uri) elif clean_style_uri: style_path = self.fetch_style_from_local_path(clean_style_uri) return style_path 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 self._first_full_path and not is_url(url): prefix, rest = self._first_full_path.split(":/") resolved = (Path(rest) / url).resolve() new_url = f"{prefix}:/{resolved}" 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 self.config.cache_dir: raise FileNotFoundError("Cache dir does not exist") response = requests.get(new_url) if not response.ok: raise FileNotFoundError(f"Error {response} fetching style URL {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 = self.config.cache_dir / f"{slugify(new_url)}.toml" self.config.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 fetch_style_from_local_path(self, partial_file_name: str) -> Optional[Path]: """Fetch a style file from a local path.""" if partial_file_name and not partial_file_name.endswith(TOML_EXTENSION): partial_file_name += TOML_EXTENSION expanded_path = Path(partial_file_name).expanduser() if not str(expanded_path).startswith("/") and self._first_full_path: # Prepend the previous path to the partial file name. style_path = Path(self._first_full_path) / expanded_path else: # Get the absolute path, be it from a root path (starting with slash) or from the current dir. style_path = Path(expanded_path).absolute() # Save the first full path to be used by the next files without parent. if not self._first_full_path: self._first_full_path = str(style_path.resolve().parent) if str(style_path) in self._already_included: return None if not style_path.exists(): raise FileNotFoundError(f"Local style file does not exist: {style_path}") LOGGER.info("Loading style from file: %s", style_path) self._already_included.add(str(style_path)) return style_path def merge_toml_dict(self) -> JsonDict: """Merge all included styles into a TOML (actually JSON) dictionary.""" return unflatten(self._all_flattened, separator=UNIQUE_SEPARATOR) PK!@ն''flake8_nitpick/types.py"""Types.""" from pathlib import Path from typing import Any, Iterator, List, MutableMapping, Tuple, Type, Union PathOrStr = Union[Path, str] JsonDict = MutableMapping[str, Any] StrOrList = Union[str, List[str]] Flake8Error = Tuple[int, int, str, Type] YieldFlake8Error = Iterator[Flake8Error] PK!H6=0flake8_nitpick-0.10.3.dist-info/entry_points.txtNINK(I+ϋ ed&g䔦gYA٩E\\PK!p̘88'flake8_nitpick-0.10.3.dist-info/LICENSEMIT License Copyright (c) 2018 Wagner Augusto Andreoli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU%flake8_nitpick-0.10.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HP4 (flake8_nitpick-0.10.3.dist-info/METADATAXks۸_Ԛ)?2ٮZ7]Oƍv:$(DRqn3\{"o !kb"cJᎯZvLEusaJnu_YՇ5ϳƧ0UzމRiF[S^ kYKWJ؆uN9Ap4d`m}{5(42k)DRM˺`d6xd"Hԟ{7Ұ5X~C2jI__=߬AVqEE^<'*6ޑ<Kbj" EHE1'<hePqDg5HSϻ~6h=_7B@p0>+%!q&sr9˳w2F}tP}*Wv.M/JizMf_:&2̲n徢E;%=04d\ ^!n 3xIqߝ;?]̗슂Ywm7?n0~JBKhnY*KR0t]3;cJ)xBó4AȒblUūt+\l]6}ɏ\ ;OQbu 5#l踋دu+44rrG Ł4/ C0鋂U'Gb#tc:sGv!˖>.̴Esܘ;:b6w2/5F]喈3XH=gq-%Ql,r-*S;m*gqZ`[Vr8|?_  A#P\&ժmZ 51So?+Dkw%mձn>O55m>C8rg>p7xcǤԁWOӰ&.s*3Ht#M9d@*1>N; O-YbyA`.mqUP6S+"Z1vhOj6όrv\>up5GSAӕd4a#qn2Gk\SnJ5'я/^`<^7P\kv}uٹLז(r=ޔ Wg7Zs#VMQ/҉ӱY%<'X7,nNp ܆-S<߻հl"Ѣ,]3taĽ_Pɗqg% ?a>QGg. ۇ+UWqNWL,vL>Hlhِߚc!!j8 .O_ ]zܘ1 ?AD0 Ft5Zq=DY.SiAOԌ+c;*(}Q1LW v|C] _{{6%)7WPbFL0< qw4QD NN&iOr݅tX=*[ a!29vm1=o!s"xcoC080mN]&0ŁSU8錙QOڟzp%y֤.وL b\C/I{rCs fu%z=Lf5= MzXDEۼ&pUpyٸ4 L;w?xN05:}xsu$c\Iu* sarOVU)@HSfZV x > Rto:\R=@hL/fkЏ$e(9z ,-?aO ? f!:F\GtD\^t'XŪ"`Es%y[ZqbӍEK.>ε&(wYTuos=~hфsf7>pX~6PK!HR X&flake8_nitpick-0.10.3.dist-info/RECORD͖:y? TBAPQ@A&YA]vY2>d$! pz !xBV Ē~4T8 45L1Xo^3"8y#Liˈh@=@'l,7d}^7 keO:uO@]ո{4 źjY"xQ.(_ ľyX4KY~arIY`NTg~>"8H߻Iгݺ+(ݛPOjwklݕj$e:9'ESZ}ү. k~,qS%?5ftw$95N?P UC(maK NY#J1z EG%??P{`=wg@p܋]X݆"f5Ū3+u8Y_Q>vQt,]w@QfldK[X/b{\>y G)ui~=OL+W{GOy>IM]AĒ02O;⃓sɱD*o@9ESyۡB%}fɡoLݒsjW$]Jusuռ& o3f-ˌDe m]%wipIjNjӏ\m̍g~'GraxHuf)枟BvV -O N^R~iڟzmx 1sRϋ $i1}cnAwѡgGV&9Asq@;p֝s܏PK!C++flake8_nitpick/__init__.pyPK!cflake8_nitpick/config.pyPK! UWflake8_nitpick/constants.pyPK! flake8_nitpick/files/__init__.pyPK!EaUZ Z flake8_nitpick/files/base.pyPK!e3''""flake8_nitpick/files/pre_commit.pyPK!4-&7flake8_nitpick/files/pyproject_toml.pyPK!4!>flake8_nitpick/files/setup_cfg.pyPK!y-BQflake8_nitpick/generic.pyPK!5hhQgflake8_nitpick/mixin.pyPK!H  iflake8_nitpick/plugin.pyPK!GWhh8tflake8_nitpick/style.pyPK!@ն''Ջflake8_nitpick/types.pyPK!H6=01flake8_nitpick-0.10.3.dist-info/entry_points.txtPK!p̘88'flake8_nitpick-0.10.3.dist-info/LICENSEPK!HڽTU%2flake8_nitpick-0.10.3.dist-info/WHEELPK!HP4 (ɒflake8_nitpick-0.10.3.dist-info/METADATAPK!HR X&Cflake8_nitpick-0.10.3.dist-info/RECORDPKnߟ