• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

repo-helper / pyproject-parser / 8552653618

04 Apr 2024 09:49AM UTC coverage: 98.538% (-0.1%) from 98.65%
8552653618

push

github

domdfcoding
Unpin docutils

876 of 889 relevant lines covered (98.54%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.51
/pyproject_parser/cli/__init__.py
1
#!/usr/bin/env python3
2
#
3
#  cli.py
4
"""
1✔
5
Command line interface.
6

7
.. versionadded:: 0.2.0
8

9
.. extras-require:: cli
10
        :pyproject:
11
"""
12
#
13
#  Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
14
#
15
#  Permission is hereby granted, free of charge, to any person obtaining a copy
16
#  of this software and associated documentation files (the "Software"), to deal
17
#  in the Software without restriction, including without limitation the rights
18
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
#  copies of the Software, and to permit persons to whom the Software is
20
#  furnished to do so, subject to the following conditions:
21
#
22
#  The above copyright notice and this permission notice shall be included in all
23
#  copies or substantial portions of the Software.
24
#
25
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
28
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
29
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
30
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
31
#  OR OTHER DEALINGS IN THE SOFTWARE.
32
#
33

34
# stdlib
35
import functools
1✔
36
import importlib
1✔
37
import re
1✔
38
import sys
1✔
39
import warnings
1✔
40
from pathlib import Path
1✔
41
from typing import TYPE_CHECKING, Optional, Pattern, TextIO, Type, Union
1✔
42

43
# 3rd party
44
import click  # nodep
1✔
45
from consolekit.tracebacks import TracebackHandler  # nodep
1✔
46
from dom_toml.parser import BadConfigError
1✔
47
from packaging.specifiers import InvalidSpecifier
1✔
48
from packaging.version import InvalidVersion
1✔
49

50
# this package
51
from pyproject_parser.utils import PyProjectDeprecationWarning
1✔
52

53
if sys.version_info >= (3, 7) or TYPE_CHECKING:
1✔
54
        # stdlib
55
        from typing import NoReturn
1✔
56

57
__all__ = ["resolve_class", "ConfigTracebackHandler", "prettify_deprecation_warning"]
1✔
58

59
class_string_re: Pattern[str] = re.compile("([A-Za-z_][A-Za-z_0-9.]+):([A-Za-z_][A-Za-z_0-9]+)")
1✔
60

61

62
def resolve_class(raw_class_string: str, name: str) -> Type:
1✔
63
        """
64
        Resolve the class name for the :option:`-P / --parser-class <pyproject-parser check -P>`
65
        and :option:`-E / --encoder-class <pyproject-parser reformat -E>` options.
66

67
        :param raw_class_string:
68
        :param name: The name of the option, e.g. ``encoder-class``. Used for error messages.
69
        """  # noqa: D400
70

71
        class_string_m = class_string_re.match(raw_class_string)
1✔
72

73
        if class_string_m:
1✔
74
                module_name, class_name = class_string_m.groups()
1✔
75
        else:
76
                raise click.BadOptionUsage(f"{name}", f"Invalid syntax for '--{name}'")
1✔
77

78
        module = importlib.import_module(module_name)
1✔
79
        resolved_class: Type = getattr(module, class_name)
1✔
80

81
        return resolved_class
1✔
82

83

84
class ConfigTracebackHandler(TracebackHandler):
1✔
85
        """
86
        :class:`consolekit.tracebacks.TracebackHandler` which handles :exc:`dom_toml.parser.BadConfigError`.
87
        """
88

89
        has_traceback_option: bool = True
1✔
90
        """
91
        Whether to show the message ``Use '--traceback' to view the full traceback.`` on error.
92
        Enabled by default.
93

94
        .. versionadded:: 0.5.0  In previous versions this was effectively :py:obj:`False`.
95
        .. versionchanged:: 0.6.0  The message is now indented with four spaces.
96
        """
97

98
        @property
1✔
99
        def _tb_option_msg(self) -> str:
1✔
100
                if self.has_traceback_option:
1✔
101
                        return "\n    Use '--traceback' to view the full traceback."
1✔
102
                else:
103
                        return ''
×
104

105
        def format_exception(self, e: Exception) -> "NoReturn":
1✔
106
                """
107
                Format the exception, showing the explanatory note and documentation link if applicable.
108

109
                .. versionadded:: 0.6.0
110

111
                :param e:
112
                """
113

114
                msg = [f"{e.__class__.__name__}: {e}"]
1✔
115
                if getattr(e, "note", None) is not None:
1✔
116
                        msg.append(f"\n    Note: {e.note}")  # type: ignore[attr-defined]
1✔
117
                if getattr(e, "documentation", None) is not None:
1✔
118
                        msg.append(f"\n    Documentation: {e.documentation}")  # type: ignore[attr-defined]
1✔
119
                msg.append(self._tb_option_msg)
1✔
120

121
                self.abort(msg)
1✔
122

123
        def handle_BadConfigError(self, e: "BadConfigError") -> "NoReturn":  # noqa: D102
1✔
124
                self.format_exception(e)
1✔
125

126
        def handle_ValueError(self, e: ValueError) -> "NoReturn":  # noqa: D102
1✔
127
                # Also covers InvalidRequirement
128
                self.format_exception(e)
1✔
129

130
        def handle_InvalidSpecifier(self, e: InvalidSpecifier) -> "NoReturn":  # noqa: D102
1✔
131
                if str(e).startswith("Invalid specifier: "):
1✔
132
                        e.args = (str(e)[len("Invalid specifier: "):], )
1✔
133
                self.format_exception(e)
1✔
134

135
        def handle_InvalidVersion(self, e: InvalidVersion) -> "NoReturn":  # noqa: D102
1✔
136
                if str(e).startswith("Invalid version: "):
1✔
137
                        e.args = (str(e)[len("Invalid version: "):], )
1✔
138
                self.format_exception(e)
1✔
139

140
        def handle_KeyError(self, e: KeyError) -> "NoReturn":  # noqa: D102
1✔
141
                self.format_exception(e)
1✔
142

143
        def handle_TypeError(self, e: TypeError) -> "NoReturn":  # noqa: D102
1✔
144
                self.format_exception(e)
1✔
145

146
        def handle_AttributeError(self, e: AttributeError) -> "NoReturn":  # noqa: D102
1✔
147
                self.format_exception(e)
1✔
148

149
        def handle_ImportError(self, e: ImportError) -> "NoReturn":  # noqa: D102
1✔
150
                self.format_exception(e)
1✔
151

152
        def handle_FileNotFoundError(self, e: FileNotFoundError) -> "NoReturn":  # noqa: D102
1✔
153
                msg = e.strerror
1✔
154

155
                no_such_file = "No such file or directory"
1✔
156

157
                if msg == "The system cannot find the file specified":
1✔
158
                        msg = no_such_file
1✔
159

160
                if msg == no_such_file:
1✔
161
                        # Probably from Python itself.
162

163
                        if e.filename is not None:
1✔
164
                                msg += f": {Path(e.filename).as_posix()!r}"
1✔
165

166
                                if e.filename2 is not None:
1✔
167
                                        msg += f" -> {Path(e.filename2).as_posix()!r}"
1✔
168

169
                        self.abort(msg)
1✔
170

171
                else:
172
                        # Probably from 3rd party code.
173
                        super().handle_FileNotFoundError(e)
1✔
174

175

176
def prettify_deprecation_warning() -> None:
1✔
177
        """
178
        Catch :class:`PyProjectDeprecationWarnings <.PyProjectDeprecationWarning>`
179
        and format them prettily for the command line.
180

181
        .. versionadded:: 0.5.0
182
        """  # noqa: D400
183

184
        orig_showwarning = warnings.showwarning
1✔
185

186
        if orig_showwarning is prettify_deprecation_warning:
1✔
187
                return
×
188

189
        @functools.wraps(warnings.showwarning)
1✔
190
        def showwarning(
1✔
191
                        message: Union[Warning, str],
192
                        category: Type[Warning],
193
                        filename: str,
194
                        lineno: int,
195
                        file: Optional[TextIO] = None,
196
                        line: Optional[str] = None,
197
                        ) -> None:
198
                if isinstance(message, PyProjectDeprecationWarning):
1✔
199
                        if file is None:
1✔
200
                                file = sys.stderr
1✔
201

202
                        s = f"WARNING: {message.args[0]}\n"
1✔
203
                        file.write(s)
1✔
204

205
                else:
206
                        orig_showwarning(message, category, filename, lineno, file, line)
×
207

208
        warnings.showwarning = showwarning
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc