diff --git a/docs/cli.rst b/docs/cli.rst index 44906c82..2312a6ea 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -75,7 +75,7 @@ At the end of execution, this command displays: difference. Return code 0 means nothing would change. Return code 1 means some files would be modified. - -v, --verbose Verbose logging + -v, --verbose Increase logging verbosity (-v = INFO, -vv = DEBUG) --help Show this message and exit. .. _cli_cmd_ls: diff --git a/src/nitpick/cli.py b/src/nitpick/cli.py index 50c44618..26891c74 100644 --- a/src/nitpick/cli.py +++ b/src/nitpick/cli.py @@ -12,6 +12,8 @@ Also see (1) from https://click.palletsprojects.com/en/5.x/setuptools/#setuptools-integration """ +import logging +import sys from pathlib import Path from typing import Optional @@ -65,7 +67,7 @@ def get_nitpick(context: click.Context) -> Nitpick: help="Don't modify the configuration files, just print the difference." " Return code 0 means nothing would change. Return code 1 means some files would be modified.", ) -@click.option("--verbose", "-v", is_flag=True, default=False, help="Verbose logging") +@click.option("--verbose", "-v", count=True, default=False, help="Increase logging verbosity (-v = INFO, -vv = DEBUG)") @click.pass_context @click.argument("files", nargs=-1) def run(context, check_only, verbose, files): @@ -74,6 +76,13 @@ def run(context, check_only, verbose, files): You can use partial and multiple file names in the FILES argument. """ if verbose: + level = logging.INFO if verbose == 1 else logging.DEBUG + + # https://loguru.readthedocs.io/en/stable/resources/recipes.html#changing-the-level-of-an-existing-handler + # https://github.com/Delgan/loguru/issues/138#issuecomment-525594566 + logger.remove() + logger.add(sys.stderr, level=logging.getLevelName(level)) + logger.enable(PROJECT_NAME) nit = get_nitpick(context) @@ -85,8 +94,8 @@ def run(context, check_only, verbose, files): click.echo(fuss.pretty) raise Exit(2) from err + click.secho(Reporter.get_counts()) if Reporter.manual or Reporter.fixed: - click.secho(Reporter.get_counts()) raise Exit(1) diff --git a/src/nitpick/core.py b/src/nitpick/core.py index 5cc78977..0ee8a3f3 100644 --- a/src/nitpick/core.py +++ b/src/nitpick/core.py @@ -79,7 +79,7 @@ def enforce_present_absent(self, *partial_names: str) -> Iterator[Fuss]: for present in (True, False): key = "present" if present else "absent" - logger.info(f"Enforce {key} files") + logger.debug(f"Enforce {key} files") absent = not present file_mapping = self.project.nitpick_files_section.get(key, {}) for filename in filter_names(file_mapping, *partial_names): @@ -109,7 +109,7 @@ def enforce_style(self, *partial_names: str, apply=True) -> Iterator[Fuss]: # 1. 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") + logger.debug(f"{config_key}: Finding plugins to enforce style") # 2. info = FileInfo.create(self.project, config_key) diff --git a/src/nitpick/project.py b/src/nitpick/project.py index 5b2cd4d3..fcc87e40 100644 --- a/src/nitpick/project.py +++ b/src/nitpick/project.py @@ -228,7 +228,7 @@ def merge_styles(self, offline: bool) -> Iterator[Fuss]: from nitpick.flake8 import NitpickFlake8Extension minimum_version = search_dict(NITPICK_MINIMUM_VERSION_JMEX, self.style_dict, None) - logger.info(f"Minimum version: {minimum_version}") + logger.debug(f"Minimum version: {minimum_version}") if minimum_version and version_to_tuple(NitpickFlake8Extension.version) < version_to_tuple(minimum_version): yield Reporter().make_fuss( ProjectViolations.MINIMUM_VERSION, diff --git a/src/nitpick/style/core.py b/src/nitpick/style/core.py index b3965f4d..c88d44c3 100644 --- a/src/nitpick/style/core.py +++ b/src/nitpick/style/core.py @@ -86,16 +86,16 @@ def get_default_style_url(): def find_initial_styles(self, configured_styles: StrOrIterable) -> Iterator[Fuss]: """Find the initial style(s) and include them.""" if configured_styles: - chosen_styles = configured_styles - log_message = f"Styles configured in {PYPROJECT_TOML}" + chosen_styles: StrOrIterable = list(configured_styles) + log_message = f"Using styles configured in {PYPROJECT_TOML}" else: paths = climb_directory_tree(self.project.root, [NITPICK_STYLE_TOML]) if paths: chosen_styles = str(sorted(paths)[0]) - log_message = "Found style climbing the directory tree" + log_message = "Using local style found climbing the directory tree" else: chosen_styles = self.get_default_style_url() - log_message = "Loading default Nitpick style" + log_message = "Using default remote Nitpick style" logger.info(f"{log_message}: {chosen_styles}") yield from self.include_multiple_styles(chosen_styles) diff --git a/src/nitpick/style/fetchers/http.py b/src/nitpick/style/fetchers/http.py index c970f09a..c339f974 100644 --- a/src/nitpick/style/fetchers/http.py +++ b/src/nitpick/style/fetchers/http.py @@ -35,7 +35,8 @@ def _do_fetch(self, url) -> str: except requests.ConnectionError as err: logger.exception(f"Request failed with {err}") click.secho( - "Your network is unreachable. Fix your connection or use" + f"The URL {url} could not be downloaded. Either your network is unreachable or the URL is broken." + f" Check the URL, fix your connection, or use " f" {OptionEnum.OFFLINE.as_flake8_flag()} / {OptionEnum.OFFLINE.as_envvar()}=1", fg="red", err=True, @@ -44,6 +45,7 @@ def _do_fetch(self, url) -> str: return contents def _download(self, url) -> str: + logger.info(f"Downloading style from {url}") response = self._session.get(url) response.raise_for_status() return response.text diff --git a/src/nitpick/violations.py b/src/nitpick/violations.py index 6e52c816..1f9ff75e 100644 --- a/src/nitpick/violations.py +++ b/src/nitpick/violations.py @@ -144,5 +144,5 @@ def get_counts(cls) -> str: if cls.manual: parts.append(f"❌ {cls.manual} to change manually") if not parts: - return "🎖 No violations found." + return "No violations found. ✨ 🍰 ✨" return f"Violations: {', '.join(parts)}." diff --git a/tests/helpers.py b/tests/helpers.py index 1771c7d5..62591bf5 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -315,34 +315,39 @@ def assert_violations(self, *expected_violations: Fuss, disclaimer="") -> "Proje compare(expected=manual, actual=Reporter.manual) return self - def _simulate_cli(self, command: str, str_or_lines: StrOrList = None, *args: str, exit_code: int = None): + def _simulate_cli(self, command: str, expected_str_or_lines: StrOrList = None, *args: str, exit_code: int = None): result = CliRunner().invoke(nitpick_cli, ["--project", str(self.root_dir), command, *args]) actual: List[str] = result.output.splitlines() - if isinstance(str_or_lines, str): - expected = dedent(str_or_lines).strip().splitlines() + if isinstance(expected_str_or_lines, str): + expected = dedent(expected_str_or_lines).strip().splitlines() else: - expected = list(always_iterable(str_or_lines)) + expected = list(always_iterable(expected_str_or_lines)) compare(actual=result.exit_code, expected=exit_code or 0) return result, actual, expected def cli_run( - self, str_or_lines: StrOrList = None, apply=False, violations=0, exception_class=None, exit_code: int = None + self, + expected_str_or_lines: StrOrList = None, + apply=False, + violations=0, + exception_class=None, + exit_code: int = None, ) -> "ProjectMock": """Assert the expected CLI output for the chosen command.""" cli_args = [] if apply else ["--check"] if exit_code is None: - exit_code = 1 if str_or_lines else 0 - result, actual, expected = self._simulate_cli("run", str_or_lines, *cli_args, exit_code=exit_code) + exit_code = 1 if expected_str_or_lines else 0 + result, actual, expected = self._simulate_cli("run", expected_str_or_lines, *cli_args, exit_code=exit_code) if exception_class: assert isinstance(result.exception, exception_class) return self if violations: expected.append(f"Violations: ❌ {violations} to change manually.") - elif str_or_lines: + elif expected_str_or_lines: # If the number of violations was not passed but a list of errors was, # remove the violation count from the actual results. # This is useful when checking only if the error is contained in a list of errors, @@ -351,6 +356,11 @@ def cli_run( if actual[-1].startswith("Violations"): del actual[-1] + if not violations and not expected_str_or_lines: + # Remove the "no violations" message + if actual[-1].startswith("No violations"): + del actual[-1] + compare(actual=actual, expected=expected) return self diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 8e016068..49366421 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -116,4 +116,7 @@ def test_offline_recommend_using_flag(tmp_path, capsys): ProjectMock(tmp_path).flake8() out, err = capsys.readouterr() assert out == "" - assert err == "Your network is unreachable. Fix your connection or use --nitpick-offline / NITPICK_OFFLINE=1\n" + assert ( + "could not be downloaded. Either your network is unreachable or the URL is broken." + " Check the URL, fix your connection, or use --nitpick-offline / NITPICK_OFFLINE=1" in err + ) diff --git a/tests/test_violations.py b/tests/test_violations.py index 331281a6..eac967ff 100644 --- a/tests/test_violations.py +++ b/tests/test_violations.py @@ -32,7 +32,7 @@ def test_reporter(): reporter.reset() assert reporter.manual == 0 assert reporter.fixed == 0 - assert reporter.get_counts() == "🎖 No violations found." + assert reporter.get_counts() == "No violations found. ✨ 🍰 ✨" reporter.increment() assert reporter.manual == 1