"""Support for ``py`` schemes."""
from __future__ import annotations
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING, Iterable, cast
import attr
import tomlkit
from furl import furl
from nitpick import PROJECT_NAME, compat
from nitpick.constants import DOT
from nitpick.style.fetchers import Scheme
from nitpick.style.fetchers.base import StyleFetcher
if TYPE_CHECKING:
from pathlib import Path
[docs]@lru_cache()
def builtin_resources_root() -> Path:
"""Built-in resources root."""
return compat.files("nitpick.resources")
[docs]@lru_cache()
def repo_root() -> Path:
"""Repository root, 3 levels up from the resources root."""
return builtin_resources_root().parent.parent.parent
[docs]def builtin_styles() -> Iterable[Path]:
"""List the built-in styles."""
yield from builtin_resources_root().glob("**/*.toml")
[docs]@dataclass(frozen=True)
class PythonPackageURL:
"""Represent a resource file in installed Python package."""
import_path: str
resource_name: str
[docs] @classmethod
def from_furl(cls, url: furl) -> PythonPackageURL:
"""Create an instance from a parsed URL in any accepted format.
See the code for ``test_parsing_python_package_urls()`` for more examples.
"""
package_name = url.host
resource_path = url.path.segments
if resource_path and not resource_path[-1]:
# strip trailing slash
*resource_path, _ = resource_path
*resource_path, resource_name = resource_path
return cls(import_path=DOT.join([package_name, *resource_path]), resource_name=resource_name)
@property
def content_path(self) -> Path:
"""Raw path of resource file."""
return compat.files(self.import_path) / self.resource_name
[docs]@dataclass(frozen=True)
class PythonPackageFetcher(StyleFetcher): # pylint: disable=too-few-public-methods
"""Fetch a style from an installed Python package.
URL schemes:
- ``py://import/path/of/style/file/<style_file_name>``
- ``pypackage://import/path/of/style/file/<style_file_name>``
E.g. ``py://some_package/path/nitpick.toml``.
"""
protocols: tuple[str, ...] = (Scheme.PY, Scheme.PYPACKAGE) # type: ignore[assignment]
def _normalize_scheme(self, scheme: str) -> str: # noqa: ARG002
# Always use the shorter py:// scheme name in the canonical URL.
return cast(str, Scheme.PY)
[docs] def fetch(self, url: furl) -> str:
"""Fetch the style from a Python package."""
package_url = PythonPackageURL.from_furl(url)
return package_url.content_path.read_text(encoding="UTF-8")
[docs]@attr.mutable(kw_only=True)
class BuiltinStyle: # pylint: disable=too-few-public-methods
"""A built-in style file in TOML format."""
py_url: furl
py_url_without_ext: furl
path_from_repo_root: str
path_from_resources_root: str
pypackage_url: PythonPackageURL = attr.field(init=False)
identify_tag: str = attr.field(init=False)
name: str = attr.field(init=False)
url: str = attr.field(init=False)
files: list[str] = attr.field(init=False)
[docs] @classmethod
def from_path(cls, resource_path: Path) -> BuiltinStyle:
"""Create a built-in style from a resource path."""
without_suffix = resource_path.with_suffix("")
src_path = builtin_resources_root().parent.parent
package_path = resource_path.relative_to(src_path)
from_resources_root = without_suffix.relative_to(builtin_resources_root())
root, *path_remainder = package_path.parts
path_remainder_without_suffix = (*path_remainder[:-1], without_suffix.parts[-1])
bis = BuiltinStyle(
py_url=furl(scheme=Scheme.PY, host=root, path=path_remainder),
py_url_without_ext=furl(scheme=Scheme.PY, host=root, path=path_remainder_without_suffix),
path_from_repo_root=resource_path.relative_to(repo_root()).as_posix(),
path_from_resources_root=from_resources_root.as_posix(),
)
bis.pypackage_url = PythonPackageURL.from_furl(bis.py_url)
bis.identify_tag = from_resources_root.parts[0]
toml_dict = tomlkit.loads(bis.pypackage_url.content_path.read_text(encoding="UTF-8"))
keys = list(toml_dict.keys())
keys.remove(PROJECT_NAME)
bis.files = keys
try:
# Intentionally break the doc generation when styles don't have [nitpick.meta]name
meta = toml_dict["nitpick"]["meta"]
bis.name = meta["name"]
bis.url = meta.get("url")
except KeyError as err:
msg = f"Style file missing [nitpick.meta] information: {bis}"
raise SyntaxError(msg) from err
return bis