Skip to content

Commit

Permalink
Create Config.annotations_for_path
Browse files Browse the repository at this point in the history
Signed-off-by: Carmen Bianca BAKKER <carmenbianca@fsfe.org>
  • Loading branch information
carmenbianca committed Jun 17, 2023
1 parent 206b68f commit 12ec884
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 1 deletion.
26 changes: 26 additions & 0 deletions src/reuse/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from dataclasses import dataclass, field
from gettext import gettext as _
from os import PathLike
from pathlib import Path, PurePath
from typing import Any, Dict, Optional

import yaml
Expand All @@ -19,6 +21,17 @@ class AnnotateOptions:
contact: Optional[str] = None
license: Optional[str] = None

def merge(self, other: "AnnotateOptions") -> "AnnotateOptions":
"""Return a copy of *self*, but replace attributes with truthy
attributes of *other*.
"""
new_kwargs = {}
for key, value in self.__dict__.items():
if other_value := getattr(other, key):
value = other_value
new_kwargs[key] = value
return self.__class__(**new_kwargs)


@dataclass
class Config:
Expand Down Expand Up @@ -68,6 +81,19 @@ def from_yaml(cls, text: str) -> "Config":
"""
return cls.from_dict(yaml.load(text, Loader=yaml.Loader))

# TODO: We could probably smartly cache the results somehow.
def annotations_for_path(self, path: PathLike) -> AnnotateOptions:
"""TODO: Document the precise behaviour."""
path = PurePath(path)
result = self.global_annotate_options
# This assumes that the override options are ordered by reverse
# precedence.
for o_path, options in self.override_annotate_options.items():
o_path = Path(o_path).expanduser()
if path.is_relative_to(o_path):
result = result.merge(options)
return result


def _annotate_options_from_dict(value: Dict[str, str]) -> AnnotateOptions:
return AnnotateOptions(
Expand Down
126 changes: 125 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,50 @@

"""Tests for some _config."""

import os
from inspect import cleandoc
from textwrap import indent
from unittest import mock

from reuse._config import Config
from reuse._config import AnnotateOptions, Config

# REUSE-IgnoreStart


def test_annotate_options_merge_one():
"""Replace one attribute."""
first = AnnotateOptions(
name="Jane Doe", contact="jane@example.com", license="MIT"
)
second = AnnotateOptions(name="John Doe")
result = first.merge(second)
assert result.name == second.name
assert result.contact == first.contact
assert result.license == first.license


def test_annotate_options_merge_nothing():
"""When merging with an empty AnnotateOptions, do nothing."""
first = AnnotateOptions(
name="Jane Doe", contact="jane@example.com", license="MIT"
)
second = AnnotateOptions()
result = first.merge(second)
assert result == first


def test_annotate_options_merge_all():
"""When merging with a full AnnotateOptions, replace all attributes."""
first = AnnotateOptions(
name="Jane Doe", contact="jane@example.com", license="MIT"
)
second = AnnotateOptions(
name="John Doe", contact="john@example.com", license="0BSD"
)
result = first.merge(second)
assert result == second


def test_config_from_dict_global_simple():
"""A simple test case for Config.from_dict."""
value = {
Expand Down Expand Up @@ -87,4 +124,91 @@ def test_config_from_yaml_simple():
)


def test_config_from_yaml_ordered():
"""The override options are ordered by appearance in the yaml file."""
overrides = []
for i in range(100):
overrides.append(
indent(
cleandoc(
f"""
- path: "{i}"
default_name: Jane Doe
"""
),
prefix=" " * 4,
)
)
text = cleandoc(
"""
annotate:
overrides:
{}
"""
).format("\n".join(overrides))
result = Config.from_yaml(text)
for i, path in enumerate(result.override_annotate_options):
assert str(i) == path


def test_annotations_for_path_global():
"""When there are no overrides, the annotate options for a given path are
always the global options.
"""
options = AnnotateOptions(name="Jane Doe")
config = Config(global_annotate_options=options)
result = config.annotations_for_path("foo")
assert result == options == config.global_annotate_options


def test_annotations_for_path_no_match():
"""When the given path doesn't match any overrides, return the global
options.
"""
global_options = AnnotateOptions(name="Jane Doe")
override_options = AnnotateOptions(name="John Doe")
config = Config(
global_annotate_options=global_options,
override_annotate_options={"~/Projects": override_options},
)
result = config.annotations_for_path("/etc/foo")
assert result == global_options


def test_annotations_for_path_one_match():
"""If one override matches, return the global options merged with the
override options.
"""
global_options = AnnotateOptions(name="Jane Doe")
override_options = AnnotateOptions(contact="jane@example.com")
config = Config(
global_annotate_options=global_options,
override_annotate_options={"/home/jane/Projects": override_options},
)
result = config.annotations_for_path(
"/home/jane/Projects/reuse-tool/README.md"
)
assert result.name == "Jane Doe"
assert result.contact == "jane@example.com"
assert not result.license


def test_annotations_for_path_expand_home():
"""When the key path of an override starts with '~', expand it when
checking.
"""
with mock.patch.dict(os.environ, {"HOME": "/home/jane"}):
global_options = AnnotateOptions(name="Jane Doe")
override_options = AnnotateOptions(contact="jane@example.com")
config = Config(
global_annotate_options=global_options,
override_annotate_options={"~/Projects": override_options},
)
result = config.annotations_for_path(
# This path must be manually expanded and cannot start with a '~'.
"/home/jane/Projects/reuse-tool/README.md"
)
assert result.contact == "jane@example.com"


# REUSE-IgnoreEnd

0 comments on commit 12ec884

Please sign in to comment.