From ab54724f16001f9d97522e16e145c13499250913 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Thu, 17 Oct 2024 15:59:37 +0200 Subject: [PATCH 1/3] Factor exceptions out into own module Signed-off-by: Carmen Bianca BAKKER --- src/reuse/__init__.py | 8 ----- src/reuse/_annotate.py | 4 +-- src/reuse/cli/common.py | 4 +-- src/reuse/comment.py | 9 +---- src/reuse/exceptions.py | 61 ++++++++++++++++++++++++++++++++++ src/reuse/global_licensing.py | 29 ++++------------ src/reuse/header.py | 13 ++------ src/reuse/project.py | 9 ++--- tests/test_comment.py | 3 +- tests/test_global_licensing.py | 6 ++-- tests/test_header.py | 10 ++---- tests/test_project.py | 9 ++--- 12 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 src/reuse/exceptions.py diff --git a/src/reuse/__init__.py b/src/reuse/__init__.py index 495f35ed6..416f7d944 100644 --- a/src/reuse/__init__.py +++ b/src/reuse/__init__.py @@ -182,11 +182,3 @@ def __bool__(self) -> bool: def __or__(self, value: "ReuseInfo") -> "ReuseInfo": return self.union(value) - - -class ReuseException(Exception): - """Base exception.""" - - -class IdentifierNotFound(ReuseException): - """Could not find SPDX identifier for license file.""" diff --git a/src/reuse/_annotate.py b/src/reuse/_annotate.py index e914f5c27..e9a89f065 100644 --- a/src/reuse/_annotate.py +++ b/src/reuse/_annotate.py @@ -30,12 +30,12 @@ ) from .comment import ( NAME_STYLE_MAP, - CommentCreateError, CommentStyle, EmptyCommentStyle, get_comment_style, ) -from .header import MissingReuseInfo, add_new_header, find_and_replace_header +from .exceptions import CommentCreateError, MissingReuseInfo +from .header import add_new_header, find_and_replace_header from .i18n import _ from .project import Project from .types import StrPath diff --git a/src/reuse/cli/common.py b/src/reuse/cli/common.py index 1f943fba1..046fd6c08 100644 --- a/src/reuse/cli/common.py +++ b/src/reuse/cli/common.py @@ -13,9 +13,9 @@ from license_expression import ExpressionError from .._util import _LICENSING -from ..global_licensing import GlobalLicensingParseError +from ..exceptions import GlobalLicensingConflict, GlobalLicensingParseError from ..i18n import _ -from ..project import GlobalLicensingConflict, Project +from ..project import Project from ..vcs import find_root diff --git a/src/reuse/comment.py b/src/reuse/comment.py index c87869333..d48e3bc45 100644 --- a/src/reuse/comment.py +++ b/src/reuse/comment.py @@ -32,19 +32,12 @@ from textwrap import dedent from typing import NamedTuple, Optional, Type, cast +from .exceptions import CommentCreateError, CommentParseError from .types import StrPath _LOGGER = logging.getLogger(__name__) -class CommentParseError(Exception): - """An error occurred during the parsing of a comment.""" - - -class CommentCreateError(Exception): - """An error occurred during the creation of a comment.""" - - class MultiLineSegments(NamedTuple): """Components that make up a multi-line comment style, e.g. '/*', '*', and '*/'. diff --git a/src/reuse/exceptions.py b/src/reuse/exceptions.py new file mode 100644 index 000000000..e82427488 --- /dev/null +++ b/src/reuse/exceptions.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""All exceptions owned by :mod:`reuse`. These exceptions all inherit +:class:`ReuseError`. +""" + +from typing import Any, Optional + + +class ReuseError(Exception): + """Base exception.""" + + +class IdentifierNotFound(ReuseError): + """Could not find SPDX identifier for license file.""" + + +class GlobalLicensingParseError(ReuseError): + """An exception representing any kind of error that occurs when trying to + parse a :class:`GlobalLicensing` file. + """ + + def __init__(self, *args: Any, source: Optional[str] = None): + super().__init__(*args) + self.source = source + + +class GlobalLicensingParseTypeError(GlobalLicensingParseError, TypeError): + """An exception representing a type error while trying to parse a + :class:`GlobalLicensing` file. + """ + + +class GlobalLicensingParseValueError(GlobalLicensingParseError, ValueError): + """An exception representing a value error while trying to parse a + :class:`GlobalLicensing` file. + """ + + +class GlobalLicensingConflict(ReuseError): + """There are two global licensing files in the project that are not + compatible. + """ + + +class MissingReuseInfo(ReuseError): + """Some REUSE information is missing from the result.""" + + +class CommentError(ReuseError): + """An error occurred during an interaction with a comment.""" + + +class CommentCreateError(Exception): + """An error occurred during the creation of a comment.""" + + +class CommentParseError(Exception): + """An error occurred during the parsing of a comment.""" diff --git a/src/reuse/global_licensing.py b/src/reuse/global_licensing.py index 3a0415294..bacc8f92a 100644 --- a/src/reuse/global_licensing.py +++ b/src/reuse/global_licensing.py @@ -33,9 +33,14 @@ from debian.copyright import Error as DebianError from license_expression import ExpressionError -from . import ReuseException, ReuseInfo, SourceType +from . import ReuseInfo, SourceType from ._util import _LICENSING from .covered_files import iter_files +from .exceptions import ( + GlobalLicensingParseError, + GlobalLicensingParseTypeError, + GlobalLicensingParseValueError, +) from .i18n import _ from .types import StrPath from .vcs import VCSStrategy @@ -72,28 +77,6 @@ class PrecedenceType(Enum): OVERRIDE = "override" -class GlobalLicensingParseError(ReuseException): - """An exception representing any kind of error that occurs when trying to - parse a :class:`GlobalLicensing` file. - """ - - def __init__(self, *args: Any, source: Optional[str] = None): - super().__init__(*args) - self.source = source - - -class GlobalLicensingParseTypeError(GlobalLicensingParseError, TypeError): - """An exception representing a type error while trying to parse a - :class:`GlobalLicensing` file. - """ - - -class GlobalLicensingParseValueError(GlobalLicensingParseError, ValueError): - """An exception representing a value error while trying to parse a - :class:`GlobalLicensing` file. - """ - - @attrs.define class _CollectionOfValidator: collection_type: Type[Collection] = attrs.field() diff --git a/src/reuse/header.py b/src/reuse/header.py index 06d838c6d..b43b38ed0 100644 --- a/src/reuse/header.py +++ b/src/reuse/header.py @@ -28,13 +28,8 @@ extract_reuse_info, merge_copyright_lines, ) -from .comment import ( - CommentCreateError, - CommentParseError, - CommentStyle, - EmptyCommentStyle, - PythonCommentStyle, -) +from .comment import CommentStyle, EmptyCommentStyle, PythonCommentStyle +from .exceptions import CommentCreateError, CommentParseError, MissingReuseInfo from .i18n import _ _LOGGER = logging.getLogger(__name__) @@ -53,10 +48,6 @@ class _TextSections(NamedTuple): after: str -class MissingReuseInfo(Exception): - """Some REUSE information is missing from the result.""" - - def _create_new_header( reuse_info: ReuseInfo, template: Optional[Template] = None, diff --git a/src/reuse/project.py b/src/reuse/project.py index 7895313fa..375adae6f 100644 --- a/src/reuse/project.py +++ b/src/reuse/project.py @@ -21,7 +21,7 @@ import attrs from binaryornot.check import is_binary -from . import IdentifierNotFound, ReuseInfo +from . import ReuseInfo from ._licenses import EXCEPTION_MAP, LICENSE_MAP from ._util import ( _LICENSEREF_PATTERN, @@ -30,6 +30,7 @@ reuse_info_of_file, ) from .covered_files import iter_files +from .exceptions import GlobalLicensingConflict, IdentifierNotFound from .global_licensing import ( GlobalLicensing, NestedReuseTOML, @@ -44,12 +45,6 @@ _LOGGER = logging.getLogger(__name__) -class GlobalLicensingConflict(Exception): - """There are two global licensing files in the project that are not - compatible. - """ - - class GlobalLicensingFound(NamedTuple): path: Path cls: Type[GlobalLicensing] diff --git a/tests/test_comment.py b/tests/test_comment.py index b0766cdfc..51c1e1247 100644 --- a/tests/test_comment.py +++ b/tests/test_comment.py @@ -13,8 +13,6 @@ import pytest from reuse.comment import ( - CommentCreateError, - CommentParseError, CommentStyle, CppCommentStyle, HtmlCommentStyle, @@ -22,6 +20,7 @@ PythonCommentStyle, _all_style_classes, ) +from reuse.exceptions import CommentCreateError, CommentParseError @pytest.fixture( diff --git a/tests/test_global_licensing.py b/tests/test_global_licensing.py index 175623916..62a69557a 100644 --- a/tests/test_global_licensing.py +++ b/tests/test_global_licensing.py @@ -15,11 +15,13 @@ from reuse import ReuseInfo, SourceType from reuse._util import _LICENSING -from reuse.global_licensing import ( - AnnotationsItem, +from reuse.exceptions import ( GlobalLicensingParseError, GlobalLicensingParseTypeError, GlobalLicensingParseValueError, +) +from reuse.global_licensing import ( + AnnotationsItem, NestedReuseTOML, PrecedenceType, ReuseDep5, diff --git a/tests/test_header.py b/tests/test_header.py index 3ef286c20..54e205bce 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -11,13 +11,9 @@ import pytest from reuse import ReuseInfo -from reuse.comment import CommentCreateError, CppCommentStyle -from reuse.header import ( - MissingReuseInfo, - add_new_header, - create_header, - find_and_replace_header, -) +from reuse.comment import CppCommentStyle +from reuse.exceptions import CommentCreateError, MissingReuseInfo +from reuse.header import add_new_header, create_header, find_and_replace_header # REUSE-IgnoreStart diff --git a/tests/test_project.py b/tests/test_project.py index 59014bf4c..1b6cd7d90 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -22,12 +22,9 @@ from reuse import ReuseInfo, SourceType from reuse._util import _LICENSING from reuse.covered_files import iter_files -from reuse.global_licensing import ( - GlobalLicensingParseError, - ReuseDep5, - ReuseTOML, -) -from reuse.project import GlobalLicensingConflict, Project +from reuse.exceptions import GlobalLicensingConflict, GlobalLicensingParseError +from reuse.global_licensing import ReuseDep5, ReuseTOML +from reuse.project import Project # REUSE-IgnoreStart From d05a806fe02939c6e5378caf0361a14aa20c5f81 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Thu, 17 Oct 2024 16:54:44 +0200 Subject: [PATCH 2/3] Standardise naming of exceptions Signed-off-by: Carmen Bianca BAKKER --- src/reuse/_annotate.py | 4 ++-- src/reuse/cli/common.py | 4 ++-- src/reuse/exceptions.py | 6 +++--- src/reuse/header.py | 23 +++++++++++++++-------- src/reuse/project.py | 21 ++++++++++++--------- tests/test_header.py | 4 ++-- tests/test_project.py | 9 ++++++--- 7 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/reuse/_annotate.py b/src/reuse/_annotate.py index e9a89f065..564c0341b 100644 --- a/src/reuse/_annotate.py +++ b/src/reuse/_annotate.py @@ -34,7 +34,7 @@ EmptyCommentStyle, get_comment_style, ) -from .exceptions import CommentCreateError, MissingReuseInfo +from .exceptions import CommentCreateError, MissingReuseInfoError from .header import add_new_header, find_and_replace_header from .i18n import _ from .project import Project @@ -152,7 +152,7 @@ def add_header_to_file( ) out.write("\n") result = 1 - except MissingReuseInfo: + except MissingReuseInfoError: out.write( _( "Error: Generated comment header for '{path}' is missing" diff --git a/src/reuse/cli/common.py b/src/reuse/cli/common.py index 046fd6c08..c2ce22132 100644 --- a/src/reuse/cli/common.py +++ b/src/reuse/cli/common.py @@ -13,7 +13,7 @@ from license_expression import ExpressionError from .._util import _LICENSING -from ..exceptions import GlobalLicensingConflict, GlobalLicensingParseError +from ..exceptions import GlobalLicensingConflictError, GlobalLicensingParseError from ..i18n import _ from ..project import Project from ..vcs import find_root @@ -60,7 +60,7 @@ def project(self) -> Project: ).format(path=error.source, message=str(error)) ) from error - except (GlobalLicensingConflict, OSError) as error: + except (GlobalLicensingConflictError, OSError) as error: raise click.UsageError(str(error)) from error self._project = project diff --git a/src/reuse/exceptions.py b/src/reuse/exceptions.py index e82427488..c24b7ffa3 100644 --- a/src/reuse/exceptions.py +++ b/src/reuse/exceptions.py @@ -13,7 +13,7 @@ class ReuseError(Exception): """Base exception.""" -class IdentifierNotFound(ReuseError): +class SpdxIdentifierNotFoundError(ReuseError): """Could not find SPDX identifier for license file.""" @@ -39,13 +39,13 @@ class GlobalLicensingParseValueError(GlobalLicensingParseError, ValueError): """ -class GlobalLicensingConflict(ReuseError): +class GlobalLicensingConflictError(ReuseError): """There are two global licensing files in the project that are not compatible. """ -class MissingReuseInfo(ReuseError): +class MissingReuseInfoError(ReuseError): """Some REUSE information is missing from the result.""" diff --git a/src/reuse/header.py b/src/reuse/header.py index b43b38ed0..6000d9a84 100644 --- a/src/reuse/header.py +++ b/src/reuse/header.py @@ -29,7 +29,11 @@ merge_copyright_lines, ) from .comment import CommentStyle, EmptyCommentStyle, PythonCommentStyle -from .exceptions import CommentCreateError, CommentParseError, MissingReuseInfo +from .exceptions import ( + CommentCreateError, + CommentParseError, + MissingReuseInfoError, +) from .i18n import _ _LOGGER = logging.getLogger(__name__) @@ -59,7 +63,8 @@ def _create_new_header( Raises: CommentCreateError: if a comment could not be created. - MissingReuseInfo: if the generated comment is missing SPDX information. + MissingReuseInfoError: if the generated comment is missing SPDX + information. """ if template is None: template = DEFAULT_TEMPLATE @@ -92,7 +97,7 @@ def _create_new_header( ) ) _LOGGER.debug(result) - raise MissingReuseInfo() + raise MissingReuseInfoError() return result @@ -116,7 +121,8 @@ def create_header( Raises: CommentCreateError: if a comment could not be created. - MissingReuseInfo: if the generated comment is missing SPDX information. + MissingReuseInfoError: if the generated comment is missing SPDX + information. """ if template is None: template = DEFAULT_TEMPLATE @@ -177,7 +183,7 @@ def _find_first_spdx_comment( preceding the comment, the comment itself, and everything following it. Raises: - MissingReuseInfo: if no REUSE info can be found in any comment + MissingReuseInfoError: if no REUSE info can be found in any comment. """ if style is None: style = PythonCommentStyle @@ -194,7 +200,7 @@ def _find_first_spdx_comment( text[:index], comment + "\n", text[index + len(comment) + 1 :] ) - raise MissingReuseInfo() + raise MissingReuseInfoError() def _extract_shebang(prefix: str, text: str) -> tuple[str, str]: @@ -239,14 +245,15 @@ def find_and_replace_header( Raises: CommentCreateError: if a comment could not be created. - MissingReuseInfo: if the generated comment is missing SPDX information. + MissingReuseInfoError: if the generated comment is missing SPDX + information. """ if style is None: style = PythonCommentStyle try: before, header, after = _find_first_spdx_comment(text, style=style) - except MissingReuseInfo: + except MissingReuseInfoError: before, header, after = "", "", text # Workaround. EmptyCommentStyle should always be completely replaced. diff --git a/src/reuse/project.py b/src/reuse/project.py index 375adae6f..cf6ca24d5 100644 --- a/src/reuse/project.py +++ b/src/reuse/project.py @@ -30,7 +30,10 @@ reuse_info_of_file, ) from .covered_files import iter_files -from .exceptions import GlobalLicensingConflict, IdentifierNotFound +from .exceptions import ( + GlobalLicensingConflictError, + SpdxIdentifierNotFoundError, +) from .global_licensing import ( GlobalLicensing, NestedReuseTOML, @@ -106,8 +109,8 @@ def from_directory( decoded. GlobalLicensingParseError: if the global licensing config file could not be parsed. - GlobalLicensingConflict: if more than one global licensing config - file is present. + GlobalLicensingConflictError: if more than one global licensing + config file is present. """ root = Path(root) if not root.exists(): @@ -318,8 +321,8 @@ def find_global_licensing( :class:`GlobalLicensing`. Raises: - GlobalLicensingConflict: if more than one global licensing config - file is present. + GlobalLicensingConflictError: if more than one global licensing + config file is present. """ candidates: list[GlobalLicensingFound] = [] dep5_path = root / ".reuse/dep5" @@ -347,7 +350,7 @@ def find_global_licensing( ] if reuse_toml_candidates: if candidates: - raise GlobalLicensingConflict( + raise GlobalLicensingConflictError( _( "Found both '{new_path}' and '{old_path}'. You" " cannot keep both files simultaneously; they are" @@ -379,13 +382,13 @@ def _identifier_of_license(self, path: Path) -> str: License Identifier. """ if not path.suffix: - raise IdentifierNotFound(f"{path} has no file extension") + raise SpdxIdentifierNotFoundError(f"{path} has no file extension") if path.stem in self.license_map: return path.stem if _LICENSEREF_PATTERN.match(path.stem): return path.stem - raise IdentifierNotFound( + raise SpdxIdentifierNotFoundError( f"Could not find SPDX License Identifier for {path}" ) @@ -413,7 +416,7 @@ def _find_licenses(self) -> dict[str, Path]: try: identifier = self._identifier_of_license(path) - except IdentifierNotFound: + except SpdxIdentifierNotFoundError: if path.name in self.license_map: _LOGGER.info( _("{path} does not have a file extension").format( diff --git a/tests/test_header.py b/tests/test_header.py index 54e205bce..15d5507db 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -12,7 +12,7 @@ from reuse import ReuseInfo from reuse.comment import CppCommentStyle -from reuse.exceptions import CommentCreateError, MissingReuseInfo +from reuse.exceptions import CommentCreateError, MissingReuseInfoError from reuse.header import add_new_header, create_header, find_and_replace_header # REUSE-IgnoreStart @@ -69,7 +69,7 @@ def test_create_header_template_no_spdx(template_no_spdx): """Create a header with a template that does not have all REUSE info.""" info = ReuseInfo({"GPL-3.0-or-later"}, {"SPDX-FileCopyrightText: Jane Doe"}) - with pytest.raises(MissingReuseInfo): + with pytest.raises(MissingReuseInfoError): create_header(info, template=template_no_spdx) diff --git a/tests/test_project.py b/tests/test_project.py index 1b6cd7d90..448a2bf79 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -22,7 +22,10 @@ from reuse import ReuseInfo, SourceType from reuse._util import _LICENSING from reuse.covered_files import iter_files -from reuse.exceptions import GlobalLicensingConflict, GlobalLicensingParseError +from reuse.exceptions import ( + GlobalLicensingConflictError, + GlobalLicensingParseError, +) from reuse.global_licensing import ReuseDep5, ReuseTOML from reuse.project import Project @@ -49,7 +52,7 @@ def test_project_conflicting_global_licensing(empty_directory): (empty_directory / "REUSE.toml").write_text("version = 1") (empty_directory / ".reuse").mkdir() shutil.copy(RESOURCES_DIRECTORY / "dep5", empty_directory / ".reuse/dep5") - with pytest.raises(GlobalLicensingConflict): + with pytest.raises(GlobalLicensingConflictError): Project.from_directory(empty_directory) @@ -602,7 +605,7 @@ def test_find_global_licensing_none(empty_directory): def test_find_global_licensing_conflict(fake_repository_dep5): """Expect an error on a conflict""" (fake_repository_dep5 / "REUSE.toml").write_text("version = 1") - with pytest.raises(GlobalLicensingConflict): + with pytest.raises(GlobalLicensingConflictError): Project.find_global_licensing(fake_repository_dep5) From 0af6fb6a9e3c92d3d963ec4d60a7109d5348e27e Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Thu, 17 Oct 2024 16:57:47 +0200 Subject: [PATCH 3/3] Fix documentation linking Signed-off-by: Carmen Bianca BAKKER --- src/reuse/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reuse/exceptions.py b/src/reuse/exceptions.py index c24b7ffa3..94d03d871 100644 --- a/src/reuse/exceptions.py +++ b/src/reuse/exceptions.py @@ -19,7 +19,7 @@ class SpdxIdentifierNotFoundError(ReuseError): class GlobalLicensingParseError(ReuseError): """An exception representing any kind of error that occurs when trying to - parse a :class:`GlobalLicensing` file. + parse a :class:`reuse.global_licensing.GlobalLicensing` file. """ def __init__(self, *args: Any, source: Optional[str] = None): @@ -29,13 +29,13 @@ def __init__(self, *args: Any, source: Optional[str] = None): class GlobalLicensingParseTypeError(GlobalLicensingParseError, TypeError): """An exception representing a type error while trying to parse a - :class:`GlobalLicensing` file. + :class:`reuse.global_licensing.GlobalLicensing` file. """ class GlobalLicensingParseValueError(GlobalLicensingParseError, ValueError): """An exception representing a value error while trying to parse a - :class:`GlobalLicensing` file. + :class:`reuse.global_licensing.GlobalLicensing` file. """