Skip to content

Commit

Permalink
feat: apply changes to setup.cfg (#288)
Browse files Browse the repository at this point in the history
* feat: apply values on setup.cfg (preserving comments)
* feat: apply missing values to a comma separated list
* feat: apply missing sections
* feat: apply missing key/value pairs
* feat: apply initial contents
* refactor: plugin entry point
* refactor: post initialisation hook
* build: pylint before pre-commit
* refactor: add "fixed" boolean flag to Fuss
* refactor: rename FileData to FileInfo
* refactor: rename file_dict to expected_config
* test: compare Fuss objects as dicts
* test: if the default style is applied on setup.cfg
* test: mark xfail on Windows
* feat: flag to configure if the plugin can use the "apply mode"
* refactor: reduce complexity of compare_different_keys()
* refactor: reduce complexity of enforce_rules()
* refactor: rename violation constants
* test: move conftest.py module imports locally inside the function
  • Loading branch information
andreoliwa authored Feb 22, 2021
1 parent 4b79f81 commit f878630
Show file tree
Hide file tree
Showing 27 changed files with 641 additions and 386 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ help:
@echo 'Run 'make -B' or 'make --always-make' to force a rebuild of all targets'
.PHONY: help

quick: pytest nitpick pre-commit pylint # Run pytest and pre-commit fast, without tox
quick: pytest nitpick pylint pre-commit # Run pytest and pre-commit fast, without tox
.PHONY: quick

full-build: .remove-old-cache .cache/make/long-pre-commit .cache/make/long-poetry .cache/make/lint .cache/make/test .cache/make/doc # Build the project fully, like in CI
Expand All @@ -40,7 +40,7 @@ install: install-pre-commit install-poetry # Install pre-commit hooks and Poetry
.PHONY: install

# Poetry install is needed to create the Nitpick plugin entries on setuptools, used by pluggy
install-poetry .cache/make/long-poetry src/nitpick.egg-info/entry_points.txt: pyproject.toml # Install Poetry dependencies
install-poetry .cache/make/long-poetry: pyproject.toml # Install Poetry dependencies
poetry install -E test -E lint
touch .cache/make/long-poetry
.PHONY: install-poetry
Expand Down Expand Up @@ -98,7 +98,7 @@ test-one .cache/make/test-one: .cache/make/long-poetry src/*/* styles/*/* tests/
touch .cache/make/test-one
.PHONY: test

pytest: src/nitpick.egg-info/entry_points.txt # Run pytest on the poetry venv (to quickly run tests locally without waiting for tox)
pytest: # Run pytest on the poetry venv (to quickly run tests locally without waiting for tox)
poetry run python -m pytest --doctest-modules
.PHONY: pytest

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
nitpick.plugins.data module
nitpick.plugins.info module
===========================

.. automodule:: nitpick.plugins.data
.. automodule:: nitpick.plugins.info
:members:
:undoc-members:
:show-inheritance:
2 changes: 1 addition & 1 deletion docs/source/nitpick.plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Submodules
:maxdepth: 4

nitpick.plugins.base
nitpick.plugins.data
nitpick.plugins.info
nitpick.plugins.json
nitpick.plugins.pre_commit
nitpick.plugins.pyproject_toml
Expand Down
26 changes: 16 additions & 10 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ sphinx_rtd_theme = {version = "*", optional = true}
pydantic = "*"
autorepr = "*"
loguru = "*"
ConfigUpdater = "*"

[tool.poetry.extras]
lint = ["pylint"]
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ deps =
pip>=19.2
safety
# Run nitpick and pylint with tox, because local repos don't seem to work well with https://pre-commit.ci/
# Run Nitpick locally on itself
commands =
# Run Nitpick locally on itself
flake8 --select=NIP
pylint src/
safety check
Expand All @@ -137,10 +137,10 @@ description = Build the HTML docs using Sphinx (sphinx-build, API docs, link che
basepython = python3.8
platform = linux|darwin
extras = doc
# Options to debug Sphinx: -nWT --keep-going -vvv
commands =
sphinx-apidoc --force --module-first --separate --implicit-namespaces --output-dir docs/source src/nitpick/
python3 docs/generate_rst.py
# Options to debug Sphinx: -nWT --keep-going -vvv
sphinx-build --color -b linkcheck docs "{toxworkdir}/docs_out"
sphinx-build -d "{toxworkdir}/docs_doctree" --color -b html docs "{toxworkdir}/docs_out" {posargs}

Expand Down
5 changes: 3 additions & 2 deletions src/nitpick/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def get_nitpick(context: click.Context) -> Nitpick:
@click.option(
"--check",
"-c",
"check_only",
is_flag=True,
default=False,
help="Don't modify the configuration files, just print the difference."
Expand All @@ -92,7 +93,7 @@ def get_nitpick(context: click.Context) -> Nitpick:
@click.option("--verbose", "-v", is_flag=True, default=False, help="Verbose logging")
@click.pass_context
@click.argument("files", nargs=-1)
def run(context, check, verbose, files):
def run(context, check_only, verbose, files):
"""Apply suggestions to configuration files.
You can use partial and multiple file names in the FILES argument.
Expand All @@ -101,7 +102,7 @@ def run(context, check, verbose, files):
logger.enable(PROJECT_NAME)

nit = get_nitpick(context)
for fuss in nit.run(*files, check=check):
for fuss in nit.run(*files, apply=not check_only):
nit.echo(fuss.pretty)

if Reporter.manual or Reporter.fixed:
Expand Down
70 changes: 34 additions & 36 deletions src/nitpick/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
from functools import lru_cache
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, List
from typing import TYPE_CHECKING, Iterator, List, Type

import click
from loguru import logger

from nitpick.constants import PROJECT_NAME
from nitpick.exceptions import QuitComplainingError
from nitpick.generic import relative_to_current_dir
from nitpick.plugins.data import FileData
from nitpick.generic import filter_names, relative_to_current_dir
from nitpick.plugins.info import FileInfo
from nitpick.project import Project
from nitpick.typedefs import PathOrStr
from nitpick.violations import Fuss, ProjectViolations, Reporter
Expand Down Expand Up @@ -51,50 +51,65 @@ def init(self, project_root: PathOrStr = None, offline: bool = None) -> "Nitpick

return self

def run(self, *partial_names: str, check=True) -> Iterator[Fuss]:
"""Run Nitpick."""
def run(self, *partial_names: str, apply=False) -> Iterator[Fuss]:
"""Run Nitpick.
:param partial_names: Names of the files to enforce configs for.
:param apply: Flag to apply changes, if the plugin supports it (default: True).
:return: Fuss generator.
"""
Reporter.reset()

try:
yield from chain(
self.project.merge_styles(self.offline),
self.enforce_present_absent(),
self.enforce_style(*partial_names, check=check),
self.enforce_present_absent(*partial_names),
self.enforce_style(*partial_names, apply=apply),
)
except QuitComplainingError as err:
yield from err.violations

def enforce_present_absent(self) -> Iterator[Fuss]:
"""Enforce files that should be present or absent."""
def enforce_present_absent(self, *partial_names: str) -> Iterator[Fuss]:
"""Enforce files that should be present or absent.
:param partial_names: Names of the files to enforce configs for.
:return: Fuss generator.
"""
if not self.project:
return

for present in (True, False):
key = "present" if present else "absent"
logger.info(f"Enforce {key} files")
absent = not present
for filename, custom_message in self.project.nitpick_files_section.get(key, {}).items():
file_mapping = self.project.nitpick_files_section.get(key, {})
for filename in filter_names(file_mapping, *partial_names):
custom_message = file_mapping[filename]
file_path: Path = self.project.root / filename
exists = file_path.exists()
if (present and exists) or (absent and not exists):
continue

reporter = Reporter(FileData.create(self.project, filename))
reporter = Reporter(FileInfo.create(self.project, filename))

extra = f": {custom_message}" if custom_message else ""
violation = ProjectViolations.MissingFile if present else ProjectViolations.FileShouldBeDeleted
violation = ProjectViolations.MISSING_FILE if present else ProjectViolations.FILE_SHOULD_BE_DELETED
yield reporter.make_fuss(violation, extra=extra)

def enforce_style(self, *partial_names: str, check=False):
def enforce_style(self, *partial_names: str, apply=True) -> Iterator[Fuss]:
"""Read the merged style and enforce the rules in it.
1. Get all root keys from the merged style
2. All except "nitpick" are file names.
3. For each file name, find the plugin(s) that can handle the file.
:param partial_names: Names of the files to enforce configs for.
:param apply: Flag to apply changes, if the plugin supports it (default: True).
:return: Fuss generator.
"""

# 1.
for config_key in self.filter_keys(*partial_names):
for config_key in filter_names(self.project.style_dict, *partial_names):
config_dict = self.project.style_dict[config_key]
logger.info(f"{config_key}: Finding plugins to enforce style")

Expand All @@ -103,31 +118,14 @@ def enforce_style(self, *partial_names: str, check=False):
continue

# 3.
for plugin_instance in self.project.plugin_manager.hook.can_handle( # pylint: disable=no-member
data=FileData.create(self.project, config_key)
): # type: NitpickPlugin
yield from plugin_instance.entry_point(config_dict, check)

def filter_keys(self, *partial_names: str) -> List[str]:
"""Filter keys, keeping only the selected partial names."""
rv = []
for key in self.project.style_dict:
if key == PROJECT_NAME:
continue

include = bool(not partial_names)
for name in partial_names:
if name in key:
include = True
break

if include:
rv.append(key)
return rv
info = FileInfo.create(self.project, config_key)
# pylint: disable=no-member
for plugin_class in self.project.plugin_manager.hook.can_handle(info=info): # type: Type[NitpickPlugin]
yield from plugin_class(info, config_dict, apply).entry_point()

def configured_files(self, *partial_names: str) -> List[Path]:
"""List of files configured in the Nitpick style. Filter only the selected partial names."""
return [Path(self.project.root) / key for key in self.filter_keys(*partial_names)]
return [Path(self.project.root) / key for key in filter_names(self.project.style_dict, *partial_names)]

def echo(self, message: str):
"""Echo a message on the terminal, with the relative path at the beginning."""
Expand Down
35 changes: 34 additions & 1 deletion src/nitpick/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import jmespath
from jmespath.parser import ParsedResult

from nitpick.constants import DOUBLE_QUOTE, SEPARATOR_FLATTEN, SEPARATOR_QUOTED_SPLIT, SINGLE_QUOTE
from nitpick.constants import DOUBLE_QUOTE, PROJECT_NAME, SEPARATOR_FLATTEN, SEPARATOR_QUOTED_SPLIT, SINGLE_QUOTE
from nitpick.typedefs import JsonDict, PathOrStr


Expand Down Expand Up @@ -241,3 +241,36 @@ def relative_to_current_dir(path_or_str: Optional[PathOrStr]) -> str:
return str(path.relative_to(Path.cwd())).lstrip(".")

return str(path.absolute())


def filter_names(iterable: Iterable, *partial_names: str) -> List[str]:
"""Filter names and keep only the desired partial names.
Exclude the project name automatically.
>>> file_list = ['requirements.txt', 'tox.ini', 'setup.py', 'nitpick']
>>> filter_names(file_list)
['requirements.txt', 'tox.ini', 'setup.py']
>>> filter_names(file_list, 'ini', '.py')
['tox.ini', 'setup.py']
>>> mapping = {'requirements.txt': None, 'tox.ini': 1, 'setup.py': 2, 'nitpick': 3}
>>> filter_names(mapping)
['requirements.txt', 'tox.ini', 'setup.py']
>>> filter_names(file_list, 'x')
['requirements.txt', 'tox.ini']
"""
rv = []
for name in iterable:
if name == PROJECT_NAME:
continue

include = bool(not partial_names)
for partial_name in partial_names:
if partial_name in name:
include = True
break

if include:
rv.append(name)
return rv
4 changes: 2 additions & 2 deletions src/nitpick/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

if TYPE_CHECKING:
from nitpick.plugins.base import NitpickPlugin
from nitpick.plugins.data import FileData
from nitpick.plugins.info import FileInfo

hookspec = pluggy.HookspecMarker(PROJECT_NAME)
hookimpl = pluggy.HookimplMarker(PROJECT_NAME)
Expand All @@ -27,7 +27,7 @@ def plugin_class() -> Type["NitpickPlugin"]:


@hookspec
def can_handle(data: "FileData") -> Optional["NitpickPlugin"]: # pylint: disable=unused-argument
def can_handle(info: "FileInfo") -> Optional[Type["NitpickPlugin"]]: # pylint: disable=unused-argument
"""Return a valid :py:class:`nitpick.plugins.base.NitpickPlugin` instance or ``None``.
:return: A plugin instance if your plugin handles this file info (path or any of its ``identify`` tags).
Expand Down
Loading

0 comments on commit f878630

Please sign in to comment.