diff --git a/flake8_nitpick/__init__.py b/flake8_nitpick/__init__.py index 23d5d17e..b3cd0583 100644 --- a/flake8_nitpick/__init__.py +++ b/flake8_nitpick/__init__.py @@ -9,6 +9,7 @@ import toml from flake8_nitpick.__version__ import __version__ +from flake8_nitpick.generic import get_subclasses, flatten, unflatten # Types NitpickError = Tuple[int, int, str, Type] @@ -16,13 +17,8 @@ # Constants ERROR_PREFIX = "NIP" PYPROJECT_TOML = "pyproject.toml" -ROOT_PYTHON_FILES = ("setup.py", "manage.py") -ROOT_FILES = ( - PYPROJECT_TOML, - "setup.cfg", - "requirements*.txt", - "Pipfile", -) + ROOT_PYTHON_FILES +ROOT_PYTHON_FILES = ("setup.py", "manage.py", "autoapp.py") +ROOT_FILES = (PYPROJECT_TOML, "setup.cfg", "requirements*.txt", "Pipfile") + ROOT_PYTHON_FILES def nitpick_error(error_number: int, error_message: str) -> NitpickError: @@ -30,15 +26,6 @@ def nitpick_error(error_number: int, error_message: str) -> NitpickError: return 1, 0, f"{ERROR_PREFIX}{error_number} {error_message}", NitpickChecker -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 - - class NitpickCache: """A cache file in the current dir (in .toml format), to store data that will be reused by the plugin.""" @@ -82,10 +69,10 @@ def __init__(self, root_dir: Path) -> None: """Init instance.""" pyproject_toml_file = root_dir / PYPROJECT_TOML toml_dict = toml.load(str(pyproject_toml_file)) - config = toml_dict.get("tool", {}).get("nitpick", {}) + self.nitpick_config = toml_dict.get("tool", {}).get("nitpick", {}) self.root_dir = root_dir - self.files: Dict[str, bool] = config.get("files", {}) + self.files: Dict[str, bool] = self.nitpick_config.get("files", {}) @attr.s(hash=False) @@ -110,17 +97,16 @@ def run(self): current_python_file = Path(self.filename) main_python_file = self.find_main_python_file(root_dir, current_python_file) if not main_python_file: - yield nitpick_error( - 100, f"No Python file was found in the root dir {root_dir}" - ) + yield nitpick_error(100, f"No Python file was found in the root dir {root_dir}") return if current_python_file.resolve() != main_python_file.resolve(): # Only report warnings once, for the main Python file of this project. return config = NitpickConfig(root_dir) - for file_checker in get_subclasses(BaseChecker): - for error in file_checker(config).check_exists(): + for checker_class in get_subclasses(BaseChecker): + checker = checker_class(config) + for error in itertools.chain(checker.check_exists(), checker.check_rules()): yield error return [] @@ -151,8 +137,7 @@ def find_main_python_file(self, root_dir: Path, current_file: Path) -> Path: return main_python_file for the_file in itertools.chain( - [root_dir / root_file for root_file in ROOT_PYTHON_FILES], - root_dir.glob("*.py"), + [root_dir / root_file for root_file in ROOT_PYTHON_FILES], root_dir.glob("*.py") ): if the_file.exists(): found = the_file @@ -165,47 +150,65 @@ def find_main_python_file(self, root_dir: Path, current_file: Path) -> Path: class BaseChecker: """Base class for file checkers.""" - filename: str + file_name: str should_exist_default: bool def __init__(self, config: NitpickConfig) -> None: """Init instance.""" self.config = config + self.file_path: Path = self.config.root_dir / self.file_name + self.file_config = self.config.nitpick_config.get(self.file_name, {}) def check_exists(self) -> Generator[List[NitpickError], Any, Any]: """Check if the file should exist or not.""" - should_exist = self.config.files.get(self.filename, self.should_exist_default) - file_exists = (self.config.root_dir / self.filename).exists() + should_exist = self.config.files.get(self.file_name, self.should_exist_default) + file_exists = self.file_path.exists() if should_exist and not file_exists: - yield nitpick_error(102, f"Missing file {self.filename!r}") + yield nitpick_error(102, f"Missing file {self.file_name!r}") elif not should_exist and file_exists: - yield nitpick_error(103, f"File {self.filename!r} should be deleted") + yield nitpick_error(103, f"File {self.file_name!r} should be deleted") + + def check_rules(self): + """Check rules for this file. It should be overridden by inherited class if they need.""" + return [] class PyProjectTomlChecker(BaseChecker): """Check pyproject.toml.""" - filename = "pyproject.toml" + file_name = "pyproject.toml" should_exist_default = True + def check_rules(self): + """Check missing key/value pairs in pyproject.toml.""" + pyproject_toml_dict = toml.load(str(self.file_path)) + actual = flatten(pyproject_toml_dict) + expected = flatten(self.file_config) + if expected.items() <= actual.items(): + return [] + + missing_dict = unflatten({k: v for k, v in expected.items() if k not in actual}) + missing_toml = toml.dumps(missing_dict) + yield nitpick_error(104, f"Missing values in {self.file_name!r}:\n{missing_toml}") + class SetupCfgChecker(BaseChecker): """Check setup.cfg.""" - filename = "setup.cfg" + file_name = "setup.cfg" should_exist_default = True class PipfileChecker(BaseChecker): """Check Pipfile.""" - filename = "Pipfile" + file_name = "Pipfile" should_exist_default = False class PipfileLockChecker(BaseChecker): """Check Pipfile.lock.""" - filename = "Pipfile.lock" + file_name = "Pipfile.lock" should_exist_default = False diff --git a/flake8_nitpick/generic.py b/flake8_nitpick/generic.py new file mode 100644 index 00000000..c7e1babc --- /dev/null +++ b/flake8_nitpick/generic.py @@ -0,0 +1,38 @@ +import collections + + +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(d, parent_key="", sep="."): + items = [] + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(flatten(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + +def unflatten(d, sep="."): + items = dict() + for k, v in d.items(): + keys = k.split(sep) + sub_items = items + for ki in keys[:-1]: + try: + sub_items = sub_items[ki] + except KeyError: + sub_items[ki] = dict() + sub_items = sub_items[ki] + + sub_items[keys[-1]] = v + + return items diff --git a/pyproject.toml b/pyproject.toml index 7650bfec..a954e8a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.black] +line-length = 120 + [tool.poetry] name = "flake8-nitpick" version = "0.1.0" @@ -24,11 +27,34 @@ pytest = "*" twine = "*" keyring = "*" wheel = "*" +pre_commit = "*" +black = {version = "*", allows-prereleases = true} +pylint = "*" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" +[tool.nitpick."pyproject.toml".tool.black] +line-length = 120 + +[tool.nitpick."pyproject.toml".tool.poetry.dev-dependencies] +black = {version = "*", allows-prereleases = true} +"flake8-blind-except" = "*" +"flake8-bugbear" = "*" +"flake8-comprehensions" = "*" +"flake8-debugger" = "*" +"flake8-docstrings" = "*" +"flake8-isort" = "*" +"flake8-mypy" = "*" +"flake8-polyfill" = "*" +"flake8-pytest" = "*" +"flake8" = "*" +pre_commit = "*" +ipython = "*" +ipdb = "*" +pylint = "*" + [tool.nitpick."setup.cfg".flake8] ignore = "D107,D202,D203,D401,E203,E402,E501,W503" max-line-length = 120 diff --git a/setup.py b/setup.py index d0672739..7a138e5e 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ VERSION = None # What packages are required for this module to be executed? -REQUIRED = ["flake8 > 3.0.0", "attrs"] +REQUIRED = ["flake8 > 3.0.0", "attrs", "toml"] # The rest you shouldn't have to touch too much :) # ------------------------------------------------