184 lines
5.6 KiB
Python
184 lines
5.6 KiB
Python
"""Module containing the logic for ``twine check``."""
|
|
# Copyright 2018 Dustin Ingram
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# https://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
import argparse
|
|
import cgi
|
|
import io
|
|
import logging
|
|
import re
|
|
from typing import List, Optional, Tuple, cast
|
|
|
|
import readme_renderer.rst
|
|
from rich import print
|
|
|
|
from twine import commands
|
|
from twine import package as package_file
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
_RENDERERS = {
|
|
None: readme_renderer.rst, # Default if description_content_type is None
|
|
"text/plain": None, # Rendering cannot fail
|
|
"text/x-rst": readme_renderer.rst,
|
|
"text/markdown": None, # Rendering cannot fail
|
|
}
|
|
|
|
|
|
# Regular expression used to capture and reformat docutils warnings into
|
|
# something that a human can understand. This is loosely borrowed from
|
|
# Sphinx: https://github.com/sphinx-doc/sphinx/blob
|
|
# /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199
|
|
_REPORT_RE = re.compile(
|
|
r"^<string>:(?P<line>(?:\d+)?): "
|
|
r"\((?P<level>DEBUG|INFO|WARNING|ERROR|SEVERE)/(\d+)?\) "
|
|
r"(?P<message>.*)",
|
|
re.DOTALL | re.MULTILINE,
|
|
)
|
|
|
|
|
|
class _WarningStream(io.StringIO):
|
|
def write(self, text: str) -> int:
|
|
matched = _REPORT_RE.search(text)
|
|
if matched:
|
|
line = matched.group("line")
|
|
level_text = matched.group("level").capitalize()
|
|
message = matched.group("message").rstrip("\r\n")
|
|
text = f"line {line}: {level_text}: {message}\n"
|
|
|
|
return super().write(text)
|
|
|
|
def __str__(self) -> str:
|
|
return self.getvalue().strip()
|
|
|
|
|
|
def _check_file(
|
|
filename: str, render_warning_stream: _WarningStream
|
|
) -> Tuple[List[str], bool]:
|
|
"""Check given distribution."""
|
|
warnings = []
|
|
is_ok = True
|
|
|
|
package = package_file.PackageFile.from_filename(filename, comment=None)
|
|
|
|
metadata = package.metadata_dictionary()
|
|
description = cast(Optional[str], metadata["description"])
|
|
description_content_type = cast(Optional[str], metadata["description_content_type"])
|
|
|
|
if description_content_type is None:
|
|
warnings.append(
|
|
"`long_description_content_type` missing. defaulting to `text/x-rst`."
|
|
)
|
|
description_content_type = "text/x-rst"
|
|
|
|
content_type, params = cgi.parse_header(description_content_type)
|
|
renderer = _RENDERERS.get(content_type, _RENDERERS[None])
|
|
|
|
if description is None or description.rstrip() == "UNKNOWN":
|
|
warnings.append("`long_description` missing.")
|
|
elif renderer:
|
|
rendering_result = renderer.render(
|
|
description, stream=render_warning_stream, **params
|
|
)
|
|
if rendering_result is None:
|
|
is_ok = False
|
|
|
|
return warnings, is_ok
|
|
|
|
|
|
def check(
|
|
dists: List[str],
|
|
strict: bool = False,
|
|
) -> bool:
|
|
"""Check that a distribution will render correctly on PyPI and display the results.
|
|
|
|
This is currently only validates ``long_description``, but more checks could be
|
|
added; see https://github.com/pypa/twine/projects/2.
|
|
|
|
:param dists:
|
|
The distribution files to check.
|
|
:param output_stream:
|
|
The destination of the resulting output.
|
|
:param strict:
|
|
If ``True``, treat warnings as errors.
|
|
|
|
:return:
|
|
``True`` if there are rendering errors, otherwise ``False``.
|
|
"""
|
|
uploads = [i for i in commands._find_dists(dists) if not i.endswith(".asc")]
|
|
if not uploads: # Return early, if there are no files to check.
|
|
logger.error("No files to check.")
|
|
return False
|
|
|
|
failure = False
|
|
|
|
for filename in uploads:
|
|
print(f"Checking {filename}: ", end="")
|
|
render_warning_stream = _WarningStream()
|
|
warnings, is_ok = _check_file(filename, render_warning_stream)
|
|
|
|
# Print the status and/or error
|
|
if not is_ok:
|
|
failure = True
|
|
print("[red]FAILED[/red]")
|
|
logger.error(
|
|
"`long_description` has syntax errors in markup"
|
|
" and would not be rendered on PyPI."
|
|
f"\n{render_warning_stream}"
|
|
)
|
|
elif warnings:
|
|
if strict:
|
|
failure = True
|
|
print("[red]FAILED due to warnings[/red]")
|
|
else:
|
|
print("[yellow]PASSED with warnings[/yellow]")
|
|
else:
|
|
print("[green]PASSED[/green]")
|
|
|
|
# Print warnings after the status and/or error
|
|
for message in warnings:
|
|
logger.warning(message)
|
|
|
|
return failure
|
|
|
|
|
|
def main(args: List[str]) -> bool:
|
|
"""Execute the ``check`` command.
|
|
|
|
:param args:
|
|
The command-line arguments.
|
|
|
|
:return:
|
|
The exit status of the ``check`` command.
|
|
"""
|
|
parser = argparse.ArgumentParser(prog="twine check")
|
|
parser.add_argument(
|
|
"dists",
|
|
nargs="+",
|
|
metavar="dist",
|
|
help="The distribution files to check, usually dist/*",
|
|
)
|
|
parser.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
default=False,
|
|
required=False,
|
|
help="Fail on warnings",
|
|
)
|
|
|
|
parsed_args = parser.parse_args(args)
|
|
|
|
# Call the check function with the arguments from the command line
|
|
return check(parsed_args.dists, strict=parsed_args.strict)
|