Skip to content

Commit

Permalink
fix: GitHub URL should preserve query args (#453)
Browse files Browse the repository at this point in the history
* refactor: remove warning: Argument 'match_querystring' is deprecated
* refactor: enum for URL schemes
* chore: create a temp tox.ini when running tasks
  • Loading branch information
andreoliwa authored Feb 6, 2022
1 parent 6ceefc8 commit a2b97b1
Show file tree
Hide file tree
Showing 14 changed files with 211 additions and 174 deletions.
6 changes: 5 additions & 1 deletion docs/autofix_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,12 @@ def _build_library(url: str = "") -> List[str]:
library: Dict[str, List[Tuple]] = defaultdict(list)
for path in sorted(builtin_styles()): # type: Path
style = BuiltinStyle.from_path(path)

# When run with tox (invoke ci-build), the path starts with "site-packages"
clean_root = style.path_from_repo_root.replace("site-packages/", "src/")

row = StyleLibraryRow(
style=f"`{style.py_url_without_ext} <{url + style.path_from_repo_root}>`_",
style=f"`{style.py_url_without_ext} <{url + clean_root}>`_",
name=f"`{style.name} <{style.url}>`_" if style.url else style.name,
)
library[style.identify_tag].append(attr.astuple(row))
Expand Down
2 changes: 1 addition & 1 deletion docs/styles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Comparing elements on lists

.. note::

At the moment, this feature only works on `the YAML plugin <yamlplugin>`_.
At the moment, this feature only works on :ref:`the YAML plugin <yamlplugin>`_.

On YAML files, a list (called a "sequence" in the YAML spec) can contain different elements:

Expand Down
181 changes: 103 additions & 78 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ cachy = "*"
importlib-resources = { version = "*", python = ">=3.7, <3.9" }
flatten-dict = "*"
dpath = "*"
furl = "*"
StrEnum = "*"

# TODO: chore: move to dependency groups once the feature is on a stable version of Poetry
# https://python-poetry.org/docs/master/managing-dependencies/#dependency-groups
Expand Down
11 changes: 7 additions & 4 deletions src/nitpick/style/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import dpath.util
from flatten_dict import flatten, unflatten
from furl import furl
from identify import identify
from loguru import logger
from more_itertools import always_iterable
Expand All @@ -27,6 +28,7 @@
PROJECT_NAME,
PROJECT_OWNER,
PYPROJECT_TOML,
SLASH,
TOML_EXTENSION,
)
from nitpick.exceptions import QuitComplainingError, pretty_exception
Expand All @@ -36,9 +38,8 @@
from nitpick.project import Project, glob_files
from nitpick.schemas import BaseStyleSchema, flatten_marshmallow_errors
from nitpick.style.config import ConfigValidator
from nitpick.style.fetchers import StyleFetcherManager
from nitpick.style.fetchers import Scheme, StyleFetcherManager
from nitpick.style.fetchers.github import GitHubURL
from nitpick.style.fetchers.pypackage import PythonPackageProtocol
from nitpick.typedefs import JsonDict, StrOrIterable, StrOrList, mypy_property
from nitpick.violations import Fuss, Reporter, StyleViolations

Expand Down Expand Up @@ -83,8 +84,10 @@ def cache_dir(self) -> Path:
def get_default_style_url(github=False):
"""Return the URL of the default style/preset."""
if github:
return GitHubURL(PROJECT_OWNER, PROJECT_NAME, f"v{__version__}", NITPICK_STYLE_TOML, None).long_protocol_url
return f"{PythonPackageProtocol.SHORT.value}://{PROJECT_NAME}/resources/presets/{PROJECT_NAME}"
return GitHubURL(PROJECT_OWNER, PROJECT_NAME, f"v{__version__}", NITPICK_STYLE_TOML).long_protocol_url

rv = furl(scheme=Scheme.PY, host=PROJECT_NAME, path=SLASH.join(["resources", "presets", PROJECT_NAME]))
return str(rv)

def find_initial_styles(self, configured_styles: StrOrIterable) -> Iterator[Fuss]:
"""Find the initial style(s) and include them."""
Expand Down
13 changes: 13 additions & 0 deletions src/nitpick/style/fetchers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
from __future__ import annotations

from dataclasses import dataclass, field
from enum import auto
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Tuple
from urllib.parse import urlparse, uses_netloc, uses_relative

from cachy import CacheManager, Repository
from strenum import LowercaseStrEnum

from nitpick.generic import is_url

Expand All @@ -17,6 +19,17 @@
StyleInfo = Tuple[Optional[Path], str]


class Scheme(LowercaseStrEnum):
"""URL schemes."""

HTTP = auto()
HTTPS = auto()
PY = auto()
PYPACKAGE = auto()
GH = auto()
GITHUB = auto()


@dataclass(repr=True)
class StyleFetcherManager:
"""Manager that controls which fetcher to be used given a protocol."""
Expand Down
8 changes: 5 additions & 3 deletions src/nitpick/style/fetchers/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Base class for fetchers that wrap inner fetchers with caching ability."""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, Tuple
from typing import Dict

from cachy import CacheManager
from loguru import logger
Expand All @@ -22,8 +24,8 @@ class StyleFetcher:
cache_option: str

requires_connection = False
protocols: Tuple[str, ...] = ()
domains: Tuple[str, ...] = ()
protocols: tuple = ()
domains: tuple[str, ...] = ()

def fetch(self, url) -> StyleInfo:
"""Fetch a style form cache or from a specific fetcher."""
Expand Down
5 changes: 3 additions & 2 deletions src/nitpick/style/fetchers/file.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Basic local file fetcher."""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Tuple

from nitpick.style.fetchers.base import StyleFetcher

Expand All @@ -10,7 +11,7 @@
class FileFetcher(StyleFetcher): # pylint: disable=too-few-public-methods
"""Fetch a style from a local file."""

protocols: Tuple[str, ...] = ("file", "")
protocols: tuple = ("file", "")

def _do_fetch(self, url):
file_path = Path(url).expanduser()
Expand Down
89 changes: 48 additions & 41 deletions src/nitpick/style/fetchers/github.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
"""Support for ``gh`` and ``github`` schemes."""
from __future__ import annotations

import os
from dataclasses import dataclass
from enum import Enum
from enum import auto
from functools import lru_cache
from typing import Tuple
from urllib.parse import parse_qs, urlparse

from furl import furl
from requests import Session, get as requests_get

from nitpick.constants import GIT_AT_REFERENCE
from nitpick.constants import GIT_AT_REFERENCE, SLASH
from nitpick.style.fetchers import Scheme
from nitpick.style.fetchers.http import HttpFetcher
from nitpick.typedefs import mypy_property


class GitHubProtocol(Enum):
"""Protocols for the GitHUb scheme."""

SHORT = "gh"
LONG = "github"


def _get_token_from_querystr(querystr) -> str:
"""Get the value of ``token`` from querystring."""
if not querystr:
return ""
query_args = parse_qs(querystr)
return query_args.get("token", [""])[0]
GITHUB_COM = "github.com"
QUERY_STRING_TOKEN = "token" # nosec


@dataclass(unsafe_hash=True)
Expand All @@ -36,12 +26,13 @@ class GitHubURL:
repository: str
git_reference: str
path: str
auth_token: str
auth_token: str | None = None
query_string: tuple | None = None

def __post_init__(self):
"""Remove the initial slash from the path."""
self._session = Session()
self.path = self.path.lstrip("/")
self.path = self.path.lstrip(SLASH)

@mypy_property
@lru_cache()
Expand All @@ -53,7 +44,7 @@ def default_branch(self) -> str:
return get_default_branch(self.api_url)

@property
def credentials(self) -> Tuple:
def credentials(self) -> tuple:
"""Credentials encoded in this URL.
A tuple of ``(api_token, '')`` if present, or empty tuple otherwise. If
Expand All @@ -68,11 +59,11 @@ def credentials(self) -> Tuple:
env_token = os.getenv(self.auth_token[1:])

if env_token:
return (env_token, "")
return env_token, ""

return ()

return (self.auth_token, "")
return self.auth_token, ""

@property
def git_reference_or_default(self) -> str:
Expand All @@ -82,30 +73,42 @@ def git_reference_or_default(self) -> str:
@property
def url(self) -> str:
"""Default URL built from attributes."""
return f"https://github.com/{self.owner}/{self.repository}/blob/{self.git_reference_or_default}/{self.path}"
rv = furl(
scheme=Scheme.HTTPS,
host=GITHUB_COM,
path=SLASH.join([self.owner, self.repository, "blob", self.git_reference_or_default, self.path]),
query_params=dict(self.query_string or {}),
)
return str(rv)

@property
def raw_content_url(self) -> str:
"""Raw content URL for this path."""
return (
f"https://raw.githubusercontent.com/{self.owner}/{self.repository}"
f"/{self.git_reference_or_default}/{self.path}"
rv = furl(
scheme=Scheme.HTTPS,
host="raw.githubusercontent.com",
path=SLASH.join([self.owner, self.repository, self.git_reference_or_default, self.path]),
)
return str(rv)

@classmethod
def parse_url(cls, url: str) -> "GitHubURL":
def parse_url(cls, url: str) -> GitHubURL:
"""Create an instance by parsing a URL string in any accepted format.
See the code for ``test_parsing_github_urls()`` for more examples.
"""
parsed_url = urlparse(url)
parsed_url = furl(url)
git_reference = ""

auth_token = parsed_url.username or _get_token_from_querystr(parsed_url.query)
auth_token = parsed_url.username or parsed_url.args.get(QUERY_STRING_TOKEN)

remaining_query_string = parsed_url.args.copy()
if QUERY_STRING_TOKEN in remaining_query_string:
del remaining_query_string[QUERY_STRING_TOKEN]

if parsed_url.scheme in GitHubFetcher.protocols:
owner = parsed_url.hostname or ""
repo_with_git_reference, path = parsed_url.path.strip("/").split("/", 1)
owner = parsed_url.host or ""
repo_with_git_reference, path = str(parsed_url.path).strip(SLASH).split(SLASH, 1)
if GIT_AT_REFERENCE in repo_with_git_reference:
repo, git_reference = repo_with_git_reference.split(GIT_AT_REFERENCE)
else:
Expand All @@ -114,31 +117,35 @@ def parse_url(cls, url: str) -> "GitHubURL":
# github.com urls have a useless .../blob/... section, but
# raw.githubusercontent.com doesn't, so take the first 2, then last 2 url
# components to exclude the blob bit if present.
owner, repo, *_, git_reference, path = parsed_url.path.strip("/").split("/", 4)
owner, repo, *_, git_reference, path = str(parsed_url.path).strip(SLASH).split(SLASH, 4)

return cls(owner, repo, git_reference, path, auth_token)
return cls(owner, repo, git_reference, path, auth_token, tuple(remaining_query_string.items()))

@property
def api_url(self) -> str:
"""API URL for this repo."""
return f"https://api.github.com/repos/{self.owner}/{self.repository}"
rv = furl(
scheme=Scheme.HTTPS, host=f"api.{GITHUB_COM}", path=SLASH.join(["repos", self.owner, self.repository])
)
return str(rv)

@property
def short_protocol_url(self) -> str:
"""Short protocol URL (``gh``)."""
return self._build_url(GitHubProtocol.SHORT)
return self._build_url(Scheme.GH)

@property
def long_protocol_url(self) -> str:
"""Long protocol URL (``github``)."""
return self._build_url(GitHubProtocol.LONG)
return self._build_url(Scheme.GITHUB)

def _build_url(self, protocol: GitHubProtocol):
def _build_url(self, scheme: auto):
if self.git_reference and self.git_reference != self.default_branch:
at_reference = f"{GIT_AT_REFERENCE}{self.git_reference}"
else:
at_reference = ""
return f"{protocol.value}://{self.owner}/{self.repository}{at_reference}/{self.path}"
rv = furl(scheme=scheme, host=self.owner, path=SLASH.join([f"{self.repository}{at_reference}", self.path]))
return str(rv)


@lru_cache()
Expand All @@ -163,8 +170,8 @@ def get_default_branch(api_url: str) -> str:
class GitHubFetcher(HttpFetcher): # pylint: disable=too-few-public-methods
"""Fetch styles from GitHub repositories."""

protocols: Tuple[str, ...] = (GitHubProtocol.SHORT.value, GitHubProtocol.LONG.value)
domains: Tuple[str, ...] = ("github.com",)
protocols: tuple = (Scheme.GH, Scheme.GITHUB)
domains: tuple[str, ...] = (GITHUB_COM,)

def _download(self, url, **kwargs) -> str:
github_url = GitHubURL.parse_url(url)
Expand Down
6 changes: 4 additions & 2 deletions src/nitpick/style/fetchers/http.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Base HTTP fetcher, other fetchers can inherit from this to wrap http errors."""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Tuple

import click
import requests
from loguru import logger
from requests.sessions import Session

from nitpick.enums import OptionEnum
from nitpick.style.fetchers import Scheme
from nitpick.style.fetchers.base import StyleFetcher


Expand All @@ -18,7 +20,7 @@ class HttpFetcher(StyleFetcher):
_session: Session = field(init=False)

requires_connection = True
protocols: Tuple[str, ...] = ("http", "https")
protocols: tuple = (Scheme.HTTP, Scheme.HTTPS)

def __post_init__(self):
"""Sessions should be per class as children can have custom headers or authentication."""
Expand Down
11 changes: 2 additions & 9 deletions src/nitpick/style/fetchers/pypackage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from functools import lru_cache
from itertools import chain
from pathlib import Path
Expand All @@ -14,6 +13,7 @@

from nitpick import PROJECT_NAME, compat
from nitpick.constants import DOT, SLASH
from nitpick.style.fetchers import Scheme
from nitpick.style.fetchers.base import StyleFetcher


Expand Down Expand Up @@ -62,13 +62,6 @@ def raw_content_url(self) -> compat.Traversable:
return compat.files(self.import_path).joinpath(self.resource_name)


class PythonPackageProtocol(Enum):
"""Protocols for the Python package scheme."""

SHORT = "py"
LONG = "pypackage"


@dataclass(repr=True, unsafe_hash=True)
class PythonPackageFetcher(StyleFetcher): # pylint: disable=too-few-public-methods
"""
Expand All @@ -81,7 +74,7 @@ class PythonPackageFetcher(StyleFetcher): # pylint: disable=too-few-public-meth
E.g. ``py://some_package/path/nitpick.toml``.
"""

protocols: tuple[str, ...] = (PythonPackageProtocol.SHORT.value, PythonPackageProtocol.LONG.value)
protocols: tuple = (Scheme.PY, Scheme.PYPACKAGE)

def _do_fetch(self, url):
package_url = PythonPackageURL.parse_url(url)
Expand Down
Loading

0 comments on commit a2b97b1

Please sign in to comment.