Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: apply changes to setup.cfg #288

Merged
merged 25 commits into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dea98d7
feat: change values on setup.cfg (preserving comments)
andreoliwa Feb 19, 2021
cc407e6
feat: add missing values to a comma separated list
andreoliwa Feb 19, 2021
d6848ef
feat: suggest missing sections
andreoliwa Feb 19, 2021
898b17b
feat: apply missing key/value pairs
andreoliwa Feb 19, 2021
2423fb5
feat: apply initial contents
andreoliwa Feb 19, 2021
7f2a0f4
feat: actually apply initial contents (it was not working)
andreoliwa Feb 19, 2021
90ee5d9
refactor: plugin entry point
andreoliwa Feb 19, 2021
d6952da
refactor: post initialization hook
andreoliwa Feb 19, 2021
71ad051
build: pylint before pre-commit
andreoliwa Feb 19, 2021
78b8eee
refactor: add "fixed" boolean flag to Fuss
andreoliwa Feb 20, 2021
99bdc9a
__wip__
andreoliwa Feb 20, 2021
574e0b9
refactor: rename check to apply, use ConfigUpdater only
andreoliwa Feb 21, 2021
e477568
refactor: rename FileData to FileInfo
andreoliwa Feb 21, 2021
2810e5f
refactor: rename file_dict to expected_config
andreoliwa Feb 21, 2021
08843cb
test: compare Fuss objects as dicts
andreoliwa Feb 21, 2021
969f8cf
test: if the default style is applied on setup.cfg
andreoliwa Feb 21, 2021
3a0f71e
test: mark xfail on Windows
andreoliwa Feb 21, 2021
d9f363d
feat: flag to configure if the plugin can use the "apply mode"
andreoliwa Feb 21, 2021
cce481b
docs: rename module
andreoliwa Feb 21, 2021
5d6922d
refactor: remove unused parser class attribute
andreoliwa Feb 21, 2021
1797c29
refactor: reduce complexity of compare_different_keys()
andreoliwa Feb 21, 2021
00820fe
refactor: reduce complexity of enforce_rules()
andreoliwa Feb 21, 2021
b8debe7
refactor: rename violation constants
andreoliwa Feb 21, 2021
db3fce8
test: move conftest.py module imports locally inside the function
andreoliwa Feb 21, 2021
7b9102c
docs: fix comment
andreoliwa Feb 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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