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

repo-helper / pyproject-parser / 17280459051

27 Aug 2025 10:38PM UTC coverage: 97.541%. First build
17280459051

push

github

domdfcoding
Add support for PEP 639 license expressions.

29 of 32 new or added lines in 3 files covered. (90.63%)

952 of 976 relevant lines covered (97.54%)

0.98 hits per line

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

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

29
# stdlib
30
import functools
1✔
31
import io
1✔
32
import os
1✔
33
import sys
1✔
34
import textwrap
1✔
35
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Optional
1✔
36

37
# 3rd party
38
import dom_toml
1✔
39
from dom_toml.parser import BadConfigError
1✔
40
from domdf_python_tools.paths import PathPlus
1✔
41
from domdf_python_tools.typing import PathLike
1✔
42

43
if TYPE_CHECKING:
44
        # this package
45
        from pyproject_parser.type_hints import ContentTypes
46

47
__all__ = [
1✔
48
                "render_markdown",
49
                "render_rst",
50
                "content_type_from_filename",
51
                "PyProjectDeprecationWarning",
52
                "indent_join",
53
                "indent_with_tab"
54
                ]
55

56

57
def render_markdown(content: str) -> None:
1✔
58
        """
59
        Attempt to render the given content as :wikipedia:`Markdown`.
60

61
        .. extras-require:: readme
62
                :pyproject:
63
                :scope: function
64

65
        :param content:
66
        """
67

68
        try:
1✔
69
                # 3rd party
70
                import cmarkgfm  # type: ignore[import]  # noqa: F401
1✔
71
                import readme_renderer.markdown
1✔
72
        except ImportError:  # pragma: no cover
73
                return
74

75
        rendering_result = readme_renderer.markdown.render(content, stream=sys.stderr)
1✔
76

77
        if rendering_result is None:  # pragma: no cover
78
                raise BadConfigError("Error rendering README.")
79

80

81
def render_rst(content: str, filename: PathLike = "<string>") -> None:
1✔
82
        """
83
        Attempt to render the given content as :wikipedia:`ReStructuredText`.
84

85
        .. extras-require:: readme
86
                :pyproject:
87
                :scope: function
88

89
        :param content:
90
        :param filename: The original filename.
91

92
        .. versionchanged:: 0.8.0  Added the ``filename`` argument.
93
        """
94

95
        try:
1✔
96
                # 3rd party
97
                import docutils.core
1✔
98
                import readme_renderer.rst
1✔
99
                from docutils.utils import SystemMessage
1✔
100
                from docutils.writers.html4css1 import Writer
1✔
101

102
        except ImportError:  # pragma: no cover
103
                return
104

105
        # Adapted from https://github.com/pypa/readme_renderer/blob/main/readme_renderer/rst.py#L106
106
        settings = readme_renderer.rst.SETTINGS.copy()
1✔
107
        settings["warning_stream"] = io.StringIO()
1✔
108

109
        writer = Writer()
1✔
110
        writer.translator_class = readme_renderer.rst.ReadMeHTMLTranslator  # type: ignore[assignment]
1✔
111

112
        try:
1✔
113
                parts = docutils.core.publish_parts(content, str(filename), writer=writer, settings_overrides=settings)
1✔
114
                if parts.get("docinfo", '') + parts.get("fragment", ''):
1✔
115
                        # Success!
116
                        return
1✔
117
        except SystemMessage:
1✔
118
                pass
1✔
119

120
        warning_stream: io.StringIO = settings["warning_stream"]  # type: ignore[assignment]
1✔
121
        if not warning_stream.tell():
1✔
122
                raise BadConfigError("Error rendering README: No content rendered from RST source.")
×
123
        else:
124
                sys.stderr.write(warning_stream.getvalue())
1✔
125
                raise BadConfigError("Error rendering README.")
1✔
126

127

128
@functools.lru_cache()
1✔
129
def content_type_from_filename(filename: PathLike) -> "ContentTypes":
1✔
130
        """
131
        Return the inferred content type for the given (readme) filename.
132

133
        :param filename:
134
        """
135

136
        filename = PathPlus(filename)
1✔
137

138
        if filename.suffix.lower() == ".md":
1✔
139
                return "text/markdown"
1✔
140
        elif filename.suffix.lower() == ".rst":
1✔
141
                return "text/x-rst"
1✔
142
        elif filename.suffix.lower() == ".txt":
1✔
143
                return "text/plain"
1✔
144

145
        raise ValueError(f"Unsupported extension for {filename.as_posix()!r}")
1✔
146

147

148
def render_readme(
1✔
149
                readme_file: PathLike,
150
                content_type: Optional["ContentTypes"] = None,
151
                encoding: str = "UTF-8",
152
                ) -> None:
153
        """
154
        Attempts to render the given readme file.
155

156
        :param readme_file:
157
        :param content_type: The content-type of the readme.
158
                If :py:obj:`None` the type will be inferred from the file extension.
159
        :param encoding: The encoding to read the file with.
160
        """
161

162
        readme_file = PathPlus(readme_file)
1✔
163

164
        if content_type is None:
1✔
165
                content_type = content_type_from_filename(filename=readme_file)
1✔
166

167
        content = readme_file.read_text(encoding=encoding)
1✔
168

169
        if int(os.environ.get("CHECK_README", 1)):
1✔
170
                if content_type == "text/markdown":
1✔
171
                        render_markdown(content)
1✔
172
                elif content_type == "text/x-rst":
1✔
173
                        render_rst(content, readme_file)
1✔
174

175

176
class PyProjectDeprecationWarning(Warning):
1✔
177
        """
178
        Warning for the use of deprecated features in `pyproject.toml`.
179

180
        This is a user-facing warning which will be shown by default.
181
        For developer-facing warnings intended for direct consumers of this library,
182
        use a standard :class:`DeprecationWarning`.
183

184
        .. versionadded:: 0.5.0
185
        """
186

187

188
def _load_toml(filename: PathLike, ) -> Dict[str, Any]:
1✔
189
        r"""
190
        Parse TOML from the given file.
191

192
        :param filename: The filename to read from to.
193

194
        :returns: A mapping containing the ``TOML`` data.
195
        """
196

197
        return dom_toml.load(filename)
1✔
198

199

200
def indent_join(iterable: Iterable[str]) -> str:
1✔
201
        """
202
        Join an iterable of strings with newlines, and indent each line with a tab if there is more then one element.
203

204
        :param iterable:
205

206
        :rtype:
207

208
        .. versionadded:: 0.14.0
209
        """
210

211
        iterable = list(iterable)
1✔
212
        if len(iterable) > 1:
1✔
NEW
213
                if not iterable[0] == '':
×
NEW
214
                        iterable.insert(0, '')
×
215

216
        return indent_with_tab(textwrap.dedent('\n'.join(iterable)))
1✔
217

218

219
def indent_with_tab(
1✔
220
                text: str,
221
                depth: int = 1,
222
                predicate: Optional[Callable[[str], bool]] = None,
223
                ) -> str:
224
        r"""
225
        Adds ``'\t'`` to the beginning of selected lines in 'text'.
226

227
        :param text: The text to indent.
228
        :param depth: The depth of the indentation.
229
        :param predicate: If given, ``'\t'``  will only be added to the lines where ``predicate(line)``
230
                is :py:obj`True`. If ``predicate`` is not provided, it will default to adding ``'\t'``
231
                to all non-empty lines that do not consist solely of whitespace characters.
232

233
        :rtype:
234

235
        .. versionadded:: 0.14.0
236
        """
237

238
        return textwrap.indent(text, '\t' * depth, predicate=predicate)
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

© 2025 Coveralls, Inc