Skip to content

Commit

Permalink
feat: library dir to scan for style files
Browse files Browse the repository at this point in the history
  • Loading branch information
andreoliwa committed Nov 20, 2023
1 parent 8db6be4 commit 7835130
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs/autofix_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def _build_library(gitref: bool = True) -> list[str]:
clean_root = path_from_repo_root.replace("site-packages/", "src/")

row = StyleLibraryRow(
style=f"{pre}`{style.py_url_without_ext} <{clean_root}>`{post}",
style=f"{pre}`{style.formatted} <{clean_root}>`{post}",
name=f"`{style.name} <{style.url}>`_" if style.url else style.name,
)
library[style.identify_tag].append(attr.astuple(row))
Expand Down
13 changes: 8 additions & 5 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,11 @@ At the end of execution, this command displays:
Create or update the [tool.nitpick] table in the configuration file.
Options:
-f, --fix Autofix the files changed by the command; otherwise, just
print what would be done
-s, --suggest Suggest styles based on the files in the project root
(skipping Git ignored files)
--help Show this message and exit.
-f, --fix Autofix the files changed by the command;
otherwise, just print what would be done
-s, --suggest Suggest styles based on the files in the project
root (skipping Git ignored files)
-l, --library DIRECTORY Library dir to scan for style files (implies
--suggest); if not provided, uses the built-in
style library
--help Show this message and exit.
9 changes: 4 additions & 5 deletions src/nitpick/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
- https://www.python.org/dev/peps/pep-0338/
- https://docs.python.org/3/using/cmdline.html#cmdoption-m
"""
# pragma: no cover
from nitpick.cli import nitpick_cli
from nitpick.constants import PROJECT_NAME
from nitpick.cli import nitpick_cli # pragma: no cover
from nitpick.constants import PROJECT_NAME # pragma: no cover


def main() -> None:
def main() -> None: # pragma: no cover
"""Entry point for the application script."""
nitpick_cli(auto_envvar_prefix=PROJECT_NAME)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
38 changes: 24 additions & 14 deletions src/nitpick/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,25 @@ def ls(context, files): # pylint: disable=invalid-name
default=False,
help="Suggest styles based on the files in the project root (skipping Git ignored files)",
)
@click.option(
"--library",
"-l",
type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True),
help="Library dir to scan for style files (implies --suggest); if not provided, uses the built-in style library",
)
@click.argument("style_urls", nargs=-1)
def init(
context,
fix: bool, # pylint: disable=redefined-outer-name
suggest: bool,
library: str | None,
style_urls: list[str],
) -> None:
"""Create or update the [tool.nitpick] table in the configuration file."""
# If a library is provided, it implies --suggest
if library:
suggest = True

if not style_urls and not suggest:
click.secho(
f"Nothing to do. {EmojiEnum.SLEEPY_FACE.value} Either pass at least one style URL"
Expand All @@ -185,25 +196,16 @@ def init(
nit = get_nitpick(context)
config = nit.project.read_configuration()

# Create the ignored styles array only when suggesting styles
if suggest and CONFIG_KEY_DONT_SUGGEST not in config.table:
config.table.add(CONFIG_KEY_DONT_SUGGEST, config.dont_suggest)

# Convert tuple to list, so we can add styles to it
style_urls = list(style_urls)
if suggest:
style_urls.extend(nit.project.suggest_styles())

new_styles = []
for style_url in style_urls:
if style_url in config.styles or style_url in config.dont_suggest:
continue
new_styles.append(style_url)
config.styles.add_line(style_url, indent=" ")

if suggest:
from nitpick import __version__ # pylint: disable=import-outside-toplevel

# Create the ignored styles array only when suggesting styles
if CONFIG_KEY_DONT_SUGGEST not in config.table:
config.table.add(CONFIG_KEY_DONT_SUGGEST, config.dont_suggest)

style_urls.extend(nit.project.suggest_styles(library))
tomlkit_ext.update_comment_before(
config.table,
CONFIG_KEY_STYLE,
Expand All @@ -215,16 +217,24 @@ def init(
""",
)

new_styles = []
for style_url in style_urls:
if style_url in config.styles or style_url in config.dont_suggest:
continue
new_styles.append(style_url)
config.styles.add_line(style_url, indent=" ")
if not new_styles:
click.echo(
f"All done! {EmojiEnum.STAR_CAKE.value} [{CONFIG_TOOL_NITPICK_KEY}] table left unchanged in {config.file.name!r}"
)
return

click.echo("New styles:")
for style in new_styles:
click.echo(f"- {style}")
count = len(new_styles)
message = f"{count} style{'s' if count > 1 else ''}"

if not fix:
click.secho(
f"Use --fix to append {message} to the [{CONFIG_TOOL_NITPICK_KEY}]"
Expand Down
24 changes: 16 additions & 8 deletions src/nitpick/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from functools import lru_cache
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING, Iterator
from typing import TYPE_CHECKING, Iterable, Iterator

import click
import tomlkit
Expand Down Expand Up @@ -350,14 +350,22 @@ def merge_styles(self, offline: bool) -> Iterator[Fuss]:
self.nitpick_section = self.style_dict.get("nitpick", {})
self.nitpick_files_section = self.nitpick_section.get("files", {})

def suggest_styles(self) -> list[str]:
def suggest_styles(self, library_path_str: PathOrStr | None) -> list[str]:
"""Suggest styles based on the files in the project root (skipping Git ignored files)."""
tags: set[str] = {ANY_BUILTIN_STYLE}
all_tags: set[str] = {ANY_BUILTIN_STYLE}
for project_file_path in glob_non_ignored_files(self.root):
tags.update(identify.tags_from_path(str(project_file_path)))
all_tags.update(identify.tags_from_path(str(project_file_path)))

if library_path_str:
library_dir = Path(library_path_str)
all_styles: Iterable[Path] = library_dir.glob("**/*.toml")
else:
library_dir = None
all_styles = builtin_styles()

suggested_styles: set[str] = set()
for style_path in builtin_styles():
builtin_style = BuiltinStyle.from_path(style_path)
if builtin_style.identify_tag in tags:
suggested_styles.add(builtin_style.py_url_without_ext.url)
for style_path in all_styles:
style = BuiltinStyle.from_path(style_path, library_dir)
if style.identify_tag in all_tags:
suggested_styles.add(style.formatted)
return sorted(suggested_styles)
31 changes: 19 additions & 12 deletions src/nitpick/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ def fetch(self, url: furl) -> str:
class BuiltinStyle: # pylint: disable=too-few-public-methods
"""A built-in style file in TOML format."""

py_url_without_ext: furl
formatted: str
path_from_resources_root: str

identify_tag: str = attr.field(init=False)
Expand All @@ -796,19 +796,26 @@ class BuiltinStyle: # pylint: disable=too-few-public-methods
files: list[str] = attr.field(init=False)

@classmethod
def from_path(cls, resource_path: Path) -> BuiltinStyle:
def from_path(cls, resource_path: Path, library_dir: Path | None = None) -> BuiltinStyle:
"""Create a style from its path."""
without_suffix = resource_path.with_suffix("")
package_path = resource_path.relative_to(builtin_resources_root().parent.parent)
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_without_ext=furl(scheme=Scheme.PY, host=root, path=path_remainder_without_suffix),
path_from_resources_root=from_resources_root.as_posix(),
)
if library_dir:
# Style in a directory
from_resources_root = without_suffix.relative_to(library_dir)
bis = BuiltinStyle(
formatted=str(without_suffix),
path_from_resources_root=from_resources_root.as_posix(),
)
else:
# Style from the built-in library
package_path = resource_path.relative_to(builtin_resources_root().parent.parent)
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(
formatted=furl(scheme=Scheme.PY, host=root, path=path_remainder_without_suffix).url,
path_from_resources_root=from_resources_root.as_posix(),
)
bis.identify_tag = from_resources_root.parts[0]
toml_dict = tomlkit.loads(resource_path.read_text(encoding="UTF-8"))

Expand Down
3 changes: 3 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def cli_init(
*,
fix: bool = False,
suggest: bool = False,
library: str | Path | None = None,
style_urls: list[str] | None = None,
exit_code: int | None = None,
) -> ProjectMock:
Expand All @@ -432,6 +433,8 @@ def cli_init(
command_args.append("--fix")
if suggest:
command_args.append("--suggest")
if library:
command_args.extend(["--library", str(library)])
if style_urls:
command_args.extend(style_urls)
if exit_code is None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_each_builtin_style(tmp_path, datadir, relative_path):
DOT_NITPICK_TOML,
f"""
[tool.nitpick]
style = "{style.py_url_without_ext}"
style = "{style.formatted}"
""",
)

Expand Down
47 changes: 47 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,50 @@ def test_added_style_should_be_the_first(
dont_suggest = []
""",
)


@pytest.mark.parametrize(
("fix", "footer"),
[
(
True,
f"The [{CONFIG_TOOL_NITPICK_KEY}] table was updated in {DOT_NITPICK_TOML!r}: 3 styles appended. {EmojiEnum.STAR_CAKE.value}",
),
(
False,
f"Use --fix to append 3 styles to the [{CONFIG_TOOL_NITPICK_KEY}] table in the config file '{DOT_NITPICK_TOML}'.",
),
],
)
def test_use_custom_library_dir(shared_datadir: Path, tmp_path: Path, fix: bool, footer: str) -> None:
"""Use a custom library directory."""
project = ProjectMock(tmp_path).save_file("README.md", "Hello")
project.cli_init(
f"""
New styles:
- {shared_datadir}/typed-style-dir/any/editorconfig
- {shared_datadir}/typed-style-dir/markdown/markdownlint
- {shared_datadir}/typed-style-dir/python/black
{footer}
""",
fix=fix,
library=shared_datadir / "typed-style-dir",
)
if fix:
project.assert_file_contents(
DOT_NITPICK_TOML,
f"""
[{CONFIG_TOOL_NITPICK_KEY}]
# nitpick-start (auto-generated by "nitpick init --suggest" {__version__})
# Styles added to the Nitpick Style Library will be appended to the end of the 'style' key.
# If you don't want a style, move it to the 'dont_suggest' key.
# nitpick-end
style = [
"{shared_datadir}/typed-style-dir/any/editorconfig",
"{shared_datadir}/typed-style-dir/markdown/markdownlint",
"{shared_datadir}/typed-style-dir/python/black",]
dont_suggest = []
""",
)
else:
assert not (tmp_path / DOT_NITPICK_TOML).exists()

0 comments on commit 7835130

Please sign in to comment.