diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7ddc64db..10a984cd 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -77,6 +77,6 @@ For merging, you should: 3. Add yourself to ``AUTHORS.rst``. .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. + `run the tests `_ for each change you add in the pull request. It will be slower though ... diff --git a/Makefile b/Makefile index 0f0b233b..bb3f7473 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ update-poetry: # Update Poetry dependencies poetry update .PHONY: update-poetry -lint .cache/make/lint: .github/*/* .travis/* docs/*.py src/*/* styles/*/* tests/*/* nitpick-style.toml .cache/make/long-poetry # Lint the project (tox running pre-commit, flake8) +lint .cache/make/lint: .github/*/* docs/*.py src/*/* styles/*/* tests/*/* nitpick-style.toml .cache/make/long-poetry # Lint the project (tox running pre-commit, flake8) tox -e lint touch .cache/make/lint .PHONY: lint diff --git a/docs/defaults.rst b/docs/defaults.rst index e88fedc2..348a7458 100644 --- a/docs/defaults.rst +++ b/docs/defaults.rst @@ -26,7 +26,7 @@ Content of `styles/absent-files.toml `_. +You can set ``style`` with any local file or URL. + +Remote style +------------ + +Use the URL of the remote file. If it's hosted on GitHub, use the raw GitHub URL: + +.. code-block:: toml + + [tool.nitpick] + style = "https://raw.githubusercontent.com/andreoliwa/nitpick/v0.23.1/nitpick-style.toml" + +You can also use the raw URL of a `GitHub Gist `_: + +.. code-block:: toml + + [tool.nitpick] + style = "https://gist.githubusercontent.com/andreoliwa/f4fccf4e3e83a3228e8422c01a48be61/raw/ff3447bddfc5a8665538ddf9c250734e7a38eabb/remote-style.toml" + +Local style +----------- Using a file in your home directory: @@ -21,6 +41,9 @@ Using a file in your home directory: [tool.nitpick] style = "~/some/path/to/another-style.toml" +Multiple styles +--------------- + You can also use multiple styles and mix local files and URLs: .. code-block:: toml @@ -32,5 +55,23 @@ You can also use multiple styles and mix local files and URLs: "https://example.com/on/the/web/third.toml" ] -The order is important: each style will override any keys that might be set by the previous ``.toml`` file. -If a key is defined in more than one file, the value from the last file will prevail. +.. note:: + + The order is important: each style will override any keys that might be set by the previous ``.toml`` file. + + If a key is defined in more than one file, the value from the last file will prevail. + +Override a remote style +----------------------- + +You can use a remote style as a starting point, and override settings on your local style file. + +Use ``./`` to indicate the local style: + +.. code-block:: toml + + [tool.nitpick] + style = [ + "https://example.com/on/the/web/remote-style.toml", + "./my-local-style.toml", + ] diff --git a/package.json b/package.json index b6090054..ff01d6ad 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "docs/conf.py", "docs/targets.rst", "docs/quickstart.rst", + "docs/tool_nitpick_section.rst", "nitpick-style.toml", "README.md", "CHANGELOG.md" diff --git a/setup.cfg b/setup.cfg index b3cde99f..c59703c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,10 @@ replace = {new_version} search = {current_version} replace = {new_version} +[bumpversion:file:docs/tool_nitpick_section.rst] +search = {current_version} +replace = {new_version} + [bumpversion:file:nitpick-style.toml] search = {current_version} replace = {new_version} @@ -151,7 +155,6 @@ parallel = true omit = tests/* .tox/* - /home/travis/virtualenv/* ; This config is needed by https://github.com/marketplace/actions/coveralls-python#usage relative_files = True diff --git a/src/nitpick/cli.py b/src/nitpick/cli.py index 36f3ae8e..67dc2c56 100644 --- a/src/nitpick/cli.py +++ b/src/nitpick/cli.py @@ -12,8 +12,6 @@ Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration """ -import os -from enum import Enum from pathlib import Path from typing import Optional @@ -23,35 +21,11 @@ from nitpick.constants import PROJECT_NAME from nitpick.core import Nitpick +from nitpick.enums import OptionEnum from nitpick.generic import relative_to_current_dir from nitpick.violations import Reporter -class _FlagMixin: - """Private mixin used to test the flags.""" - - name: str - - def as_flake8_flag(self) -> str: - """Format the name of a flag to be used on the CLI.""" - slug = self.name.lower().replace("_", "-") - return f"--{PROJECT_NAME}-{slug}" - - def as_envvar(self) -> str: - """Format the name of an environment variable.""" - return f"{PROJECT_NAME.upper()}_{self.name.upper()}" - - def get_environ(self) -> str: - """Get the value of an environment variable.""" - return os.environ.get(self.as_envvar(), "") - - -class NitpickFlag(_FlagMixin, Enum): - """Flags to be used with the CLI.""" - - OFFLINE = "Offline mode: no style will be downloaded (no HTTP requests at all)" - - @click.group() @click.option( "--project", @@ -60,10 +34,10 @@ class NitpickFlag(_FlagMixin, Enum): help="Path to project root", ) @click.option( - f"--{NitpickFlag.OFFLINE.name.lower()}", # pylint: disable=no-member + f"--{OptionEnum.OFFLINE.name.lower()}", # pylint: disable=no-member is_flag=True, default=False, - help=NitpickFlag.OFFLINE.value, + help=OptionEnum.OFFLINE.value, ) def nitpick_cli(project: Path = None, offline=False): # pylint: disable=unused-argument """Enforce the same configuration across multiple projects.""" diff --git a/src/nitpick/constants.py b/src/nitpick/constants.py index 51cfeda9..367284ce 100644 --- a/src/nitpick/constants.py +++ b/src/nitpick/constants.py @@ -1,13 +1,15 @@ """Constants.""" +import os + import jmespath PROJECT_NAME = "nitpick" FLAKE8_PREFIX = "NIP" CACHE_DIR_NAME = ".cache" TOML_EXTENSION = ".toml" -NITPICK_STYLE_TOML = "nitpick-style{}".format(TOML_EXTENSION) -MERGED_STYLE_TOML = "merged-style{}".format(TOML_EXTENSION) -RAW_GITHUB_CONTENT_BASE_URL = "https://raw.githubusercontent.com/andreoliwa/{}".format(PROJECT_NAME) +NITPICK_STYLE_TOML = f"nitpick-style{TOML_EXTENSION}" +MERGED_STYLE_TOML = f"merged-style{TOML_EXTENSION}" +RAW_GITHUB_CONTENT_BASE_URL = f"https://raw.githubusercontent.com/andreoliwa/{PROJECT_NAME}" READ_THE_DOCS_URL = "https://nitpick.rtfd.io/en/latest/" # Special files @@ -35,3 +37,7 @@ 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") + +#: Dot/slash is used to indicate a local style file +SLASH = os.path.sep +DOT_SLASH = f".{SLASH}" diff --git a/src/nitpick/enums.py b/src/nitpick/enums.py new file mode 100644 index 00000000..23ade224 --- /dev/null +++ b/src/nitpick/enums.py @@ -0,0 +1,30 @@ +"""Enums.""" +import os +from enum import Enum + +from nitpick import PROJECT_NAME + + +class _OptionMixin: + """Private mixin used to test the CLI options.""" + + name: str + + def as_flake8_flag(self) -> str: + """Format the name of a flag to be used on the CLI.""" + slug = self.name.lower().replace("_", "-") + return f"--{PROJECT_NAME}-{slug}" + + def as_envvar(self) -> str: + """Format the name of an environment variable.""" + return f"{PROJECT_NAME.upper()}_{self.name.upper()}" + + def get_environ(self) -> str: + """Get the value of an environment variable.""" + return os.environ.get(self.as_envvar(), "") + + +class OptionEnum(_OptionMixin, Enum): + """Options to be used with the CLI.""" + + OFFLINE = "Offline mode: no style will be downloaded (no HTTP requests at all)" diff --git a/src/nitpick/exceptions.py b/src/nitpick/exceptions.py index f33b4611..a5c4669c 100644 --- a/src/nitpick/exceptions.py +++ b/src/nitpick/exceptions.py @@ -30,8 +30,7 @@ def pre_commit_without_dash(path_from_root: str) -> bool: if path_from_root == PreCommitPlugin.filename[1:]: warnings.warn( - 'The section name for dotfiles should start with a dot: [".{}"]'.format(path_from_root), - DeprecationWarning, + f'The section name for dotfiles should start with a dot: [".{path_from_root}"]', DeprecationWarning ) return True diff --git a/src/nitpick/fields.py b/src/nitpick/fields.py index a9cdeac3..9f0755dd 100644 --- a/src/nitpick/fields.py +++ b/src/nitpick/fields.py @@ -57,14 +57,14 @@ def validate_section_dot_field(section_field: str) -> bool: """Validate if the combination section/field has a dot separating them.""" common = "Use ." if "." not in section_field: - raise ValidationError("Dot is missing. {}".format(common)) + raise ValidationError(f"Dot is missing. {common}") parts = section_field.split(".") if len(parts) > 2: - raise ValidationError("There's more than one dot. {}".format(common)) + raise ValidationError(f"There's more than one dot. {common}") if not parts[0].strip(): - raise ValidationError("Empty section name. {}".format(common)) + raise ValidationError(f"Empty section name. {common}") if not parts[1].strip(): - raise ValidationError("Empty field name. {}".format(common)) + raise ValidationError(f"Empty field name. {common}") return True diff --git a/src/nitpick/flake8.py b/src/nitpick/flake8.py index 0a8303e7..5e3de764 100644 --- a/src/nitpick/flake8.py +++ b/src/nitpick/flake8.py @@ -9,9 +9,9 @@ from loguru import logger from nitpick import __version__ -from nitpick.cli import NitpickFlag from nitpick.constants import FLAKE8_PREFIX, PROJECT_NAME from nitpick.core import Nitpick +from nitpick.enums import OptionEnum from nitpick.exceptions import QuitComplainingError from nitpick.typedefs import Flake8Error from nitpick.violations import Fuss @@ -64,7 +64,7 @@ def collect_errors(self) -> Iterator[Fuss]: def add_options(option_manager: OptionManager): """Add the offline option.""" option_manager.add_option( - NitpickFlag.OFFLINE.as_flake8_flag(), action="store_true", help=NitpickFlag.OFFLINE.value + OptionEnum.OFFLINE.as_flake8_flag(), action="store_true", help=OptionEnum.OFFLINE.value ) @staticmethod @@ -76,5 +76,5 @@ def parse_options(option_manager: OptionManager, options, args): # pylint: disa log_mapping = {1: logging.INFO, 2: logging.DEBUG} logging.basicConfig(level=log_mapping.get(options.verbose, logging.WARNING)) - nit = Nitpick.singleton().init(offline=bool(options.nitpick_offline or NitpickFlag.OFFLINE.get_environ())) + nit = Nitpick.singleton().init(offline=bool(options.nitpick_offline or OptionEnum.OFFLINE.get_environ())) logger.info("Offline mode: {}", nit.offline) diff --git a/src/nitpick/generic.py b/src/nitpick/generic.py index f8d40503..386d972d 100644 --- a/src/nitpick/generic.py +++ b/src/nitpick/generic.py @@ -45,7 +45,7 @@ def flatten(dict_, parent_key="", separator=".", current_lists=None) -> JsonDict items = [] # type: List[Tuple[str, Any]] for key, value in dict_.items(): - quoted_key = "{quote}{key}{quote}".format(quote=DOUBLE_QUOTE, key=key) if separator in str(key) else key + quoted_key = f"{DOUBLE_QUOTE}{key}{DOUBLE_QUOTE}" if separator in str(key) else key new_key = str(parent_key) + separator + str(quoted_key) if parent_key else quoted_key if isinstance(value, dict): items.extend(flatten(value, new_key, separator, current_lists).items()) @@ -83,7 +83,7 @@ def quoted_split(string_: str, separator=".") -> List[str]: return string_.split(separator) quoted_regex = re.compile( - "([{single}{double}][^{single}{double}]+[{single}{double}])".format(single=SINGLE_QUOTE, double=DOUBLE_QUOTE) + f"([{SINGLE_QUOTE}{DOUBLE_QUOTE}][^{SINGLE_QUOTE}{DOUBLE_QUOTE}]+[{SINGLE_QUOTE}{DOUBLE_QUOTE}])" ) def remove_quotes(match): diff --git a/src/nitpick/plugins/pre_commit.py b/src/nitpick/plugins/pre_commit.py index d37581fb..db6d080a 100644 --- a/src/nitpick/plugins/pre_commit.py +++ b/src/nitpick/plugins/pre_commit.py @@ -31,7 +31,7 @@ class PreCommitHook: @property def unique_key(self) -> str: """Unique key of this hook, to be used in a dict.""" - return "{}_{}".format(self.repo, self.hook_id) + return f"{self.repo}_{self.hook_id}" @property def key_value_pair(self) -> Tuple[str, "PreCommitHook"]: @@ -52,7 +52,7 @@ def get_all_hooks_from(cls, str_or_yaml: Union[str, YamlData]): for index, hook in enumerate(repo.get(KEY_HOOKS, [])): repo_data_only = repo.copy() repo_data_only.pop(KEY_HOOKS) - hook_data_only = search_dict("{}[{}]".format(KEY_HOOKS, index), repo, {}) + hook_data_only = search_dict(f"{KEY_HOOKS}[{index}]", repo, {}) repo_data_only.update({KEY_HOOKS: [hook_data_only]}) hooks.append( PreCommitHook(repo.get(KEY_REPO), hook[KEY_ID], YAMLFormat(data=[repo_data_only])).key_value_pair @@ -154,7 +154,7 @@ def enforce_repo_block(self, expected_repo_block: OrderedDict) -> Iterator[Fuss] # Display the current revision of the hook current_revision = comparison.flat_actual.get("rev", None) - revision_message = " (rev: {})".format(current_revision) if current_revision else "" + revision_message = f" (rev: {current_revision})" if current_revision else "" yield from self.warn_missing_different(comparison, f": hook {hook.hook_id!r}{revision_message}") def enforce_repo_old_format(self, index: int, repo_data: OrderedDict) -> Iterator[Fuss]: @@ -202,9 +202,9 @@ def format_hook(expected_dict) -> str: output: List[str] = [] for line in lines.split("\n"): if line.startswith("id:"): - output.insert(0, " - {}".format(line)) + output.insert(0, f" - {line}") else: - output.append(" {}".format(line)) + output.append(f" {line}") return "\n".join(output) diff --git a/src/nitpick/project.py b/src/nitpick/project.py index b0ad80e4..40d9150c 100644 --- a/src/nitpick/project.py +++ b/src/nitpick/project.py @@ -139,7 +139,7 @@ def find_main_python_file(self) -> Path: # TODO: add unit tests # 1. [self.root / root_file for root_file in ROOT_PYTHON_FILES], # 2. - self.root.glob("*/{}".format(MANAGE_PY)), + self.root.glob(f"*/{MANAGE_PY}"), # 3. self.root.glob("*.py"), self.root.glob("*/*.py"), diff --git a/src/nitpick/schemas.py b/src/nitpick/schemas.py index a0400328..6f2ebfcb 100644 --- a/src/nitpick/schemas.py +++ b/src/nitpick/schemas.py @@ -15,11 +15,9 @@ def flatten_marshmallow_errors(errors: Dict) -> str: formatted = [] for field, data in SortedDict(flatten(errors)).items(): if isinstance(data, list): - messages_per_field = ["{}: {}".format(field, ", ".join(data))] # TODO: .format() to f-strings + messages_per_field = [f"{field}: {', '.join(data)}"] elif isinstance(data, dict): - messages_per_field = [ - "{}[{}]: {}".format(field, index, ", ".join(messages)) for index, messages in data.items() - ] + messages_per_field = [f"{field}[{index}]: {', '.join(messages)}" for index, messages in data.items()] else: # This should never happen; if it does, let's just convert to a string messages_per_field = [str(errors)] @@ -30,7 +28,7 @@ def flatten_marshmallow_errors(errors: Dict) -> str: def help_message(sentence: str, help_page: str) -> str: """Show help with the documentation URL on validation errors.""" clean_sentence = sentence.strip(" .") - return "{}. See {}{}.".format(clean_sentence, READ_THE_DOCS_URL, help_page) + return f"{clean_sentence}. See {READ_THE_DOCS_URL}{help_page}." class BaseNitpickSchema(Schema): diff --git a/src/nitpick/style.py b/src/nitpick/style.py index 03cd4999..dae93a7d 100644 --- a/src/nitpick/style.py +++ b/src/nitpick/style.py @@ -18,6 +18,7 @@ from nitpick import __version__, fields from nitpick.constants import ( CACHE_DIR_NAME, + DOT_SLASH, MERGED_STYLE_TOML, NITPICK_STYLE_TOML, NITPICK_STYLES_INCLUDE_JMEX, @@ -26,6 +27,7 @@ RAW_GITHUB_CONTENT_BASE_URL, TOML_EXTENSION, ) +from nitpick.enums import OptionEnum from nitpick.exceptions import Deprecation, QuitComplainingError, pretty_exception from nitpick.formats import TOMLFormat from nitpick.generic import MergeDict, is_url, search_dict @@ -175,12 +177,19 @@ 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) + remote = None + if clean_style_uri.startswith(DOT_SLASH): + remote = False + elif is_url(clean_style_uri) or is_url(self._first_full_path): + remote = True elif clean_style_uri: - style_path = self.fetch_style_from_local_path(clean_style_uri) - return style_path + remote = False + + if remote is True: + return self.fetch_style_from_url(clean_style_uri) + if remote is False: + return self.fetch_style_from_local_path(clean_style_uri) + return None def fetch_style_from_url(self, url: str) -> Optional[Path]: """Fetch a style file from a URL, saving the contents in the cache dir.""" @@ -209,18 +218,15 @@ def fetch_style_from_url(self, url: str) -> Optional[Path]: try: response = requests.get(new_url) except requests.ConnectionError: - from nitpick.cli import NitpickFlag # pylint: disable=import-outside-toplevel - click.secho( - "Your network is unreachable. Fix your connection or use {} / {}=1".format( - NitpickFlag.OFFLINE.as_flake8_flag(), NitpickFlag.OFFLINE.as_envvar() - ), + "Your network is unreachable. Fix your connection or use" + f" {OptionEnum.OFFLINE.as_flake8_flag()} / {OptionEnum.OFFLINE.as_envvar()}=1", fg="red", err=True, ) return None if not response.ok: - raise FileNotFoundError("Error {} fetching style URL {}".format(response, new_url)) + 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: @@ -242,7 +248,7 @@ def fetch_style_from_local_path(self, partial_filename: str) -> Optional[Path]: partial_filename += TOML_EXTENSION expanded_path = Path(partial_filename).expanduser() - if not str(expanded_path).startswith("/") and self._first_full_path: + if self._first_full_path and not (str(expanded_path).startswith("/") or partial_filename.startswith(DOT_SLASH)): # Prepend the previous path to the partial file name. style_path = Path(self._first_full_path) / expanded_path else: diff --git a/src/nitpick/violations.py b/src/nitpick/violations.py index bc35becc..5656fac2 100644 --- a/src/nitpick/violations.py +++ b/src/nitpick/violations.py @@ -31,7 +31,7 @@ class Fuss: @property def colored_suggestion(self) -> str: """Suggestion with color.""" - return click.style("\n{}".format(self.suggestion.rstrip()), fg="green") if self.suggestion else "" + return click.style(f"\n{self.suggestion.rstrip()}", fg="green") if self.suggestion else "" @property def pretty(self) -> str: diff --git a/styles/absent-files.toml b/styles/absent-files.toml index 15808d52..91a1b5e5 100644 --- a/styles/absent-files.toml +++ b/styles/absent-files.toml @@ -4,4 +4,4 @@ "Pipfile" = "Use pyproject.toml instead" "Pipfile.lock" = "Use pyproject.toml instead" ".venv" = "" -".pyup.yml" = "Configure .travis.yml with safety instead: https://github.com/pyupio/safety#using-safety-with-a-ci-service" +".pyup.yml" = "Configure safety instead: https://github.com/pyupio/safety#using-safety-with-a-ci-service" diff --git a/tests/helpers.py b/tests/helpers.py index 3ae00e8d..54138020 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -69,7 +69,7 @@ def create_symlink(self, link_name: str, target_dir: Path = None, target_file: s path = self.root_dir / link_name # type: Path full_source_path = Path(target_dir or self.fixtures_dir) / (target_file or link_name) if not full_source_path.exists(): - raise RuntimeError("Source file does not exist: {}".format(full_source_path)) + raise RuntimeError(f"Source file does not exist: {full_source_path}") path.symlink_to(full_source_path) if path.suffix == ".py": self.files_to_lint.append(path) @@ -162,7 +162,7 @@ def named_style(self, filename: PathOrStr, file_contents: str) -> "ProjectMock": @staticmethod def ensure_toml_extension(filename: PathOrStr) -> PathOrStr: """Ensure a file name has the .toml extension.""" - return filename if str(filename).endswith(".toml") else "{}.toml".format(filename) + return filename if str(filename).endswith(".toml") else f"{filename}.toml" def setup_cfg(self, file_contents: str) -> "ProjectMock": """Save setup.cfg.""" @@ -198,7 +198,7 @@ def _assert_error_count(self, expected_error: str, expected_count: int = None) - if expected_count is not None: actual = len(self._flake8_errors_as_string) if expected_count != actual: - self.raise_assertion_error(expected_error, "Expected {} errors, got {}".format(expected_count, actual)) + self.raise_assertion_error(expected_error, f"Expected {expected_count} errors, got {actual}") return self def assert_errors_contain(self, raw_error: str, expected_count: int = None) -> "ProjectMock": diff --git a/tests/test_config.py b/tests/test_config.py index 4cd169cb..cb1bef28 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -39,7 +39,7 @@ def test_no_python_file_root_dir(tmp_path): .simulate_run(api=False) ) project.assert_single_error( - "NIP102 No Python file was found on the root dir and subdir of {!r}".format(str(project.root_dir)) + f"NIP102 No Python file was found on the root dir and subdir of {str(project.root_dir)!r}" ) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 6ed550e2..af006ceb 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -7,9 +7,9 @@ import requests from flake8.main import cli -from nitpick.cli import _FlagMixin from nitpick.constants import READ_THE_DOCS_URL from nitpick.core import Nitpick +from nitpick.enums import _OptionMixin from nitpick.violations import Fuss from tests.helpers import ProjectMock @@ -92,7 +92,7 @@ def test_present_files(tmp_path): def test_flag_format_env_variable(): """Test flag formatting and env variable.""" - class OtherFlags(_FlagMixin, Enum): + class OtherFlags(_OptionMixin, Enum): """Some flags to be used on the assertions below.""" MULTI_WORD = 1 diff --git a/tests/test_style.py b/tests/test_style.py index 4c34bf6d..d108e9cc 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -8,7 +8,8 @@ import pytest import responses -from nitpick.constants import READ_THE_DOCS_URL, TOML_EXTENSION +from nitpick.constants import DOT_SLASH, PYPROJECT_TOML, READ_THE_DOCS_URL, TOML_EXTENSION +from nitpick.violations import Fuss from tests.helpers import XFAIL_ON_WINDOWS, ProjectMock, assert_conditions if TYPE_CHECKING: @@ -196,21 +197,21 @@ def test_relative_and_other_root_dirs(offline, tmp_path): project = ( ProjectMock(tmp_path) .named_style( - "{}/main".format(another_dir), + f"{another_dir}/main", """ [nitpick.styles] include = "styles/pytest.toml" """, ) .named_style( - "{}/styles/pytest".format(another_dir), + f"{another_dir}/styles/pytest", """ ["pyproject.toml".tool.pytest] some-option = 123 """, ) .named_style( - "{}/styles/black".format(another_dir), + f"{another_dir}/styles/black", """ ["pyproject.toml".tool.black] line-length = 99 @@ -218,7 +219,7 @@ def test_relative_and_other_root_dirs(offline, tmp_path): """, ) .named_style( - "{}/poetry".format(another_dir), + f"{another_dir}/poetry", """ ["pyproject.toml".tool.poetry] version = "1.0" @@ -235,13 +236,11 @@ def test_relative_and_other_root_dirs(offline, tmp_path): # Use full path on initial styles project.pyproject_toml( - """ + f""" [tool.nitpick] style = ["{another_dir}/main", "{another_dir}/styles/black"] {common_pyproject} - """.format( - another_dir=another_dir, common_pyproject=common_pyproject - ) + """ ).simulate_run(offline=offline).assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -252,13 +251,11 @@ def test_relative_and_other_root_dirs(offline, tmp_path): # Reuse the first full path that appears project.pyproject_toml( - """ + f""" [tool.nitpick] - style = ["{}/main", "styles/black.toml"] - {} - """.format( - another_dir, common_pyproject - ) + style = ["{another_dir}/main", "styles/black.toml"] + {common_pyproject} + """ ).simulate_run().assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -269,13 +266,11 @@ def test_relative_and_other_root_dirs(offline, tmp_path): # Allow relative paths project.pyproject_toml( - """ + f""" [tool.nitpick] - style = ["{}/styles/black", "../poetry"] - {} - """.format( - another_dir, common_pyproject - ) + style = ["{another_dir}/styles/black", "../poetry"] + {common_pyproject} + """ ).simulate_run(offline=offline).assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -346,7 +341,7 @@ def test_relative_style_on_urls(tmp_path): """, } for filename, body in mapping.items(): - responses.add(responses.GET, "{}/{}.toml".format(base_url, filename), dedent(body), status=200) + responses.add(responses.GET, f"{base_url}/{filename}.toml", dedent(body), status=200) project = ProjectMock(tmp_path) @@ -358,13 +353,11 @@ def test_relative_style_on_urls(tmp_path): """ # Use full path on initial styles project.pyproject_toml( - """ + f""" [tool.nitpick] style = ["{base_url}/main", "{base_url}/styles/black.toml"] {common_pyproject} - """.format( - base_url=base_url, common_pyproject=common_pyproject - ) + """ ).simulate_run().assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -375,13 +368,11 @@ def test_relative_style_on_urls(tmp_path): # Reuse the first full path that appears project.pyproject_toml( - """ + f""" [tool.nitpick] - style = ["{}/main.toml", "styles/black"] - {} - """.format( - base_url, common_pyproject - ) + style = ["{base_url}/main.toml", "styles/black"] + {common_pyproject} + """ ).simulate_run().assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -392,13 +383,11 @@ def test_relative_style_on_urls(tmp_path): # Allow relative paths project.pyproject_toml( - """ + f""" [tool.nitpick] - style = ["{}/styles/black.toml", "../poetry"] - {} - """.format( - base_url, common_pyproject - ) + style = ["{base_url}/styles/black.toml", "../poetry"] + {common_pyproject} + """ ).simulate_run().assert_single_error( """ NIP318 File pyproject.toml has missing values:\x1b[32m @@ -411,12 +400,54 @@ def test_relative_style_on_urls(tmp_path): ) +@responses.activate +@XFAIL_ON_WINDOWS +def test_local_style_should_override_settings(tmp_path): + """Don't build relative URLs from local file names (starting with "./").""" + remote_url = "https://example.com/remote-style.toml" + remote_style = """ + ["pyproject.toml".tool.black] + line-length = 100 + """ + responses.add(responses.GET, remote_url, dedent(remote_style), status=200) + + local_file = "local-file.toml" + local_style = """ + ["pyproject.toml".tool.black] + line-length = 120 + """ + + ProjectMock(tmp_path).pyproject_toml( + f""" + [tool.nitpick] + style = [ + "{remote_url}", + "{DOT_SLASH}{local_file}", + ] + + [tool.black] + line-length = 80 + """ + ).named_style(local_file, local_style).api_check().assert_violations( + Fuss( + False, + PYPROJECT_TOML, + 319, + " has different values. Use this:", + """ + [tool.black] + line-length = 120 + """, + ) + ) + + @responses.activate def test_fetch_private_github_urls(tmp_path): """Fetch private GitHub URLs with a token on the query string.""" base_url = "https://raw.githubusercontent.com/user/private_repo/branch/path/to/nitpick-style" query_string = "?token=xxx" - full_private_url = "{}{}{}".format(base_url, TOML_EXTENSION, query_string) + full_private_url = f"{base_url}{TOML_EXTENSION}{query_string}" body = """ ["pyproject.toml".tool.black] missing = "thing" @@ -424,12 +455,10 @@ def test_fetch_private_github_urls(tmp_path): responses.add(responses.GET, full_private_url, dedent(body), status=200) project = ProjectMock(tmp_path).pyproject_toml( - """ + f""" [tool.nitpick] - style = "{}{}" - """.format( - base_url, query_string - ) + style = "{base_url}{query_string}" + """ ) project.simulate_run(offline=False).assert_single_error( """ @@ -519,9 +548,9 @@ def test_invalid_tool_nitpick_on_pyproject_toml(offline, tmp_path): "style.1: Shorter than minimum length 1.\nstyle.2: Shorter than minimum length 1.", ), ]: - project.pyproject_toml("[tool.nitpick]\n{}".format(style)).simulate_run(offline=offline).assert_errors_contain( + project.pyproject_toml(f"[tool.nitpick]\n{style}").simulate_run(offline=offline).assert_errors_contain( "NIP001 File pyproject.toml has an incorrect style." - + " Invalid data in [tool.nitpick]:\x1b[32m\n{}\x1b[0m".format(error_message), + + f" Invalid data in [tool.nitpick]:\x1b[32m\n{error_message}\x1b[0m", 1, ) @@ -563,16 +592,14 @@ def test_invalid_nitpick_files(offline, tmp_path): ).simulate_run( offline=offline ).assert_errors_contain( - """ + f""" NIP001 File some_style.toml has an incorrect style. Invalid config:\x1b[32m - xxx: Unknown file. See https://nitpick.rtfd.io/en/latest/plugins.html.\x1b[0m + xxx: Unknown file. See {READ_THE_DOCS_URL}plugins.html.\x1b[0m """ ).assert_errors_contain( - """ + f""" NIP001 File wrong_files.toml has an incorrect style. Invalid config:\x1b[32m - nitpick.files.whatever: Unknown file. See {}nitpick_section.html#nitpick-files.\x1b[0m - """.format( - READ_THE_DOCS_URL - ), + nitpick.files.whatever: Unknown file. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.\x1b[0m + """, 2, )