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: Resolve relative URIs in nitpick.styles.include #470

Merged
merged 2 commits into from
Mar 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 23 additions & 1 deletion docs/nitpick_section.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ Example of usage: :gitref:`Nitpick's default style <nitpick-style.toml>`.
[nitpick.styles]
include = ["styles/python37", "styles/poetry"]
The styles will be merged following the sequence in the list.
The styles will be merged following the sequence in the list. The ``.toml``
extension for each referenced file can be onitted.
andreoliwa marked this conversation as resolved.
Show resolved Hide resolved

Relative references are resolved relative to the URI of the style doument they
are included in according to the `normal rules of RFC 3986 <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.2>`_.

E.g. for a style file located at
``gh://$GITHUB_TOKEN@foo_dev/bar_project@branchname/styles/foobar.toml`` the following
strings all reference the exact same canonical location to include:

.. code-block:: toml
[nitpick.styles]
include = [
"foobar.toml",
"../styles/foobar.toml",
"/bar_project@branchname/styles/foobar.toml",
"//$GITHUB_TOKEN@foo_dev/bar_project@branchname/styles/foobar.toml",
]
For style files on the local filesystem, the canonical path
(after symbolic links have been resolved) of the style file is used as the
base.

If a key/value pair appears in more than one sub-style, it will be overridden; the last declared key/pair will prevail.
4 changes: 0 additions & 4 deletions src/nitpick/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@
NITPICK_STYLES_INCLUDE_JMEX = jmespath.compile("nitpick.styles.include")
NITPICK_MINIMUM_VERSION_JMEX = jmespath.compile("nitpick.minimum_version")

# Dot/slash is just a convention to indicate a local style file;
# the same pair of characters should be used on Windows, even though the path separator is different
DOT = "."
SLASH = "/"
DOT_SLASH = f"{DOT}{SLASH}"

GIT_AT_REFERENCE = "@"

Expand Down
49 changes: 25 additions & 24 deletions src/nitpick/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"""
from __future__ import annotations

import re
import sys
from pathlib import Path
from typing import Iterable

import furl

from nitpick.constants import DOT, PROJECT_NAME
from nitpick.typedefs import PathOrStr

URL_RE = re.compile(r"[a-z]+://[\$\w]\w*")


def version_to_tuple(version: str = None) -> tuple[int, ...]:
"""Transform a version number into a tuple of integers, for comparison.
Expand Down Expand Up @@ -43,27 +43,6 @@ def version_to_tuple(version: str = None) -> tuple[int, ...]:
return tuple(int(part) for part in clean_version.split(DOT))


def is_url(url: str) -> bool:
"""Return True if a string is a URL.

>>> is_url("")
False
>>> is_url(" ")
False
>>> is_url("http://example.com")
True
>>> is_url("github://andreoliwa/nitpick/styles/black")
True
>>> is_url("github://$VARNAME@andreoliwa/nitpick/styles/black")
True
>>> is_url("github://LitErAl@andreoliwa/nitpick/styles/black")
True
>>> is_url("github://notfirst$@andreoliwa/nitpick/styles/black")
True
"""
return bool(URL_RE.match(url))


def relative_to_current_dir(path_or_str: PathOrStr | None) -> str:
"""Return a relative path to the current dir or an absolute path."""
if not path_or_str:
Expand Down Expand Up @@ -108,3 +87,25 @@ def filter_names(iterable: Iterable, *partial_names: str) -> list[str]:
if include:
rv.append(name)
return rv


if sys.platform == "win32":

def furl_path_to_python_path(path: furl.Path) -> Path:
"""Convert a file URL to a path."""
drive, *segments = path.segments
return Path(f"{drive}/", *segments)

def get_scheme(url: str) -> str | None:
"""Get the scheme of a URL, or None if there is no scheme."""
scheme = furl.get_scheme(url)
# Windows drive letters are not a valid scheme
return scheme if scheme and len(scheme) > 1 else None

else:

def furl_path_to_python_path(path: furl.Path) -> Path:
"""Convert a file URL to a path."""
return Path("/", *path.segments)

get_scheme = furl.get_scheme
41 changes: 24 additions & 17 deletions src/nitpick/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@

import itertools
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Iterable, Iterator

import pluggy
from autorepr import autorepr
from loguru import logger
from marshmallow_polyfield import PolyField
Expand Down Expand Up @@ -36,22 +34,22 @@
from nitpick.exceptions import QuitComplainingError
from nitpick.generic import version_to_tuple
from nitpick.schemas import BaseNitpickSchema, flatten_marshmallow_errors, help_message
from nitpick.typedefs import JsonDict, PathOrStr, mypy_property
from nitpick.typedefs import JsonDict, PathOrStr
from nitpick.violations import Fuss, ProjectViolations, Reporter, StyleViolations


def glob_files(dir_: Path, file_patterns: Iterable[str]) -> set[Path]:
"""Search a directory looking for file patterns."""
for pattern in file_patterns:
found_files = list(dir_.glob(pattern))
found_files = set(dir_.glob(pattern))
if found_files:
return set(found_files)
return found_files
return set()


def confirm_project_root(dir_: PathOrStr | None = None) -> Path:
"""Confirm this is the root dir of the project (the one that has one of the ``ROOT_FILES``)."""
possible_root_dir = Path(dir_ or Path.cwd())
possible_root_dir = Path(dir_ or Path.cwd()).resolve()
root_files = glob_files(possible_root_dir, ROOT_FILES)
logger.debug(f"Root files found: {root_files}")

Expand Down Expand Up @@ -110,27 +108,35 @@ class Project:

__repr__ = autorepr(["_chosen_root", "root"])

_plugin_manager: PluginManager
_confirmed_root: Path

def __init__(self, root: PathOrStr = None) -> None:
self._chosen_root = root

self.style_dict: JsonDict = {}
self.nitpick_section: JsonDict = {}
self.nitpick_files_section: JsonDict = {}

@mypy_property
@lru_cache()
@property
andreoliwa marked this conversation as resolved.
Show resolved Hide resolved
def root(self) -> Path:
"""Root dir of the project."""
return confirm_project_root(self._chosen_root)
try:
root = self._confirmed_root
except AttributeError:
root = self._confirmed_root = confirm_project_root(self._chosen_root)
return root

@mypy_property
@lru_cache()
@property
def plugin_manager(self) -> PluginManager:
"""Load all defined plugins."""
plugin_manager = pluggy.PluginManager(PROJECT_NAME)
plugin_manager.add_hookspecs(plugins)
plugin_manager.load_setuptools_entrypoints(PROJECT_NAME)
return plugin_manager
try:
manager = self._plugin_manager
except AttributeError:
manager = self._plugin_manager = PluginManager(PROJECT_NAME)
manager.add_hookspecs(plugins)
manager.load_setuptools_entrypoints(PROJECT_NAME)
return manager
andreoliwa marked this conversation as resolved.
Show resolved Hide resolved

def read_configuration(self) -> Configuration:
"""Search for a configuration file and validate it against a Marshmallow schema."""
Expand Down Expand Up @@ -175,7 +181,8 @@ def merge_styles(self, offline: bool) -> Iterator[Fuss]:
from nitpick.style import StyleManager

style = StyleManager(self, offline, config.cache)
style_errors = list(style.find_initial_styles(peekable(always_iterable(config.styles))))
base = config.file.expanduser().resolve().as_uri() if config.file else None
style_errors = list(style.find_initial_styles(peekable(always_iterable(config.styles)), base))
if style_errors:
raise QuitComplainingError(style_errors)

Expand Down Expand Up @@ -209,7 +216,7 @@ def create_configuration(self, config: Configuration) -> None:
tool_nitpick = table()
tool_nitpick.add(comment("Generated by the 'nitpick init' command"))
tool_nitpick.add(comment(f"More info at {READ_THE_DOCS_URL}configuration.html"))
tool_nitpick.add("style", [StyleManager.get_default_style_url()])
tool_nitpick.add("style", [str(StyleManager.get_default_style_url())])
doc.add(SingleKey(TOOL_NITPICK_KEY, KeyType.Bare), tool_nitpick)

# config.file will always have a value at this point, but mypy can't see it.
Expand Down
Loading