Skip to content

Commit

Permalink
Merge e3efa1e into a2c812d
Browse files Browse the repository at this point in the history
  • Loading branch information
domdfcoding committed May 3, 2022
2 parents a2c812d + e3efa1e commit d98192d
Show file tree
Hide file tree
Showing 18 changed files with 354 additions and 35 deletions.
10 changes: 10 additions & 0 deletions pyproject_parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

# this package
from pyproject_parser import License
from pyproject_parser.cli import prettify_deprecation_warning

if TYPE_CHECKING:
# 3rd party
Expand Down Expand Up @@ -108,6 +109,9 @@ def check(
from pyproject_parser.cli import ConfigTracebackHandler, resolve_class
from pyproject_parser.parsers import BuildSystemParser, PEP621Parser

if not show_traceback:
prettify_deprecation_warning()

pyproject_file = PathPlus(pyproject_file)

click.echo(f"Validating {str(pyproject_file)!r}")
Expand Down Expand Up @@ -204,6 +208,9 @@ def info(
from pyproject_parser.cli import ConfigTracebackHandler, resolve_class
from pyproject_parser.type_hints import Readme

if not show_traceback:
prettify_deprecation_warning()

pyproject_file = PathPlus(pyproject_file)

with handle_tracebacks(show_traceback, ConfigTracebackHandler):
Expand Down Expand Up @@ -293,6 +300,9 @@ def reformat(
from pyproject_parser import PyProject, _NormalisedName
from pyproject_parser.cli import ConfigTracebackHandler, resolve_class

if not show_traceback:
prettify_deprecation_warning()

pyproject_file = PathPlus(pyproject_file)

click.echo(f"Reformatting {str(pyproject_file)!r}")
Expand Down
36 changes: 35 additions & 1 deletion pyproject_parser/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@
#

# stdlib
import functools
import importlib
import re
import sys
import warnings
from typing import Pattern, Type

# 3rd party
Expand All @@ -42,7 +45,10 @@
from consolekit.utils import abort # nodep
from dom_toml.parser import BadConfigError

__all__ = ["resolve_class", "ConfigTracebackHandler"]
# this package
from pyproject_parser.utils import PyProjectDeprecationWarning

__all__ = ["resolve_class", "ConfigTracebackHandler", "prettify_deprecation_warning"]

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

Expand Down Expand Up @@ -103,3 +109,31 @@ def handle_AttributeError(self, e: AttributeError) -> bool: # noqa: D102

def handle_ImportError(self, e: ImportError) -> bool: # noqa: D102
raise abort(f"{e.__class__.__name__}: {e}{self._tb_option_msg}", colour=False)


def prettify_deprecation_warning() -> None:
"""
Catch :class:`PyProjectDeprecationWarnings <~.PyProjectDeprecationWarning>`
and format them prettily for the command line.
.. versionadded:: $VERSION
""" # noqa: D400

orig_showwarning = warnings.showwarning

if orig_showwarning is prettify_deprecation_warning:
return

@functools.wraps(warnings.showwarning)
def showwarning(message, category, filename, lineno, file=None, line=None):
if isinstance(message, PyProjectDeprecationWarning):
if file is None:
file = sys.stderr

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

else:
orig_showwarning(message, category, filename, lineno, file, line)

warnings.showwarning = showwarning
63 changes: 51 additions & 12 deletions pyproject_parser/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import collections.abc
import os
import re
import warnings
from abc import ABCMeta
from typing import Any, Callable, ClassVar, Dict, Iterable, List, Mapping, Set, Union, cast

Expand All @@ -47,7 +48,7 @@
# this package
from pyproject_parser.classes import License, Readme, _NormalisedName
from pyproject_parser.type_hints import Author, BuildSystemDict, ProjectDict
from pyproject_parser.utils import content_type_from_filename, render_readme
from pyproject_parser.utils import PyProjectDeprecationWarning, content_type_from_filename, render_readme

__all__ = [
"RequiredKeysConfigParser",
Expand All @@ -56,6 +57,7 @@
]

name_re = re.compile("^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", flags=re.IGNORECASE)
extra_re = re.compile("^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$")


class RequiredKeysConfigParser(AbstractConfigParser, metaclass=ABCMeta):
Expand Down Expand Up @@ -926,33 +928,70 @@ def parse_optional_dependencies(
]
:param config: The unparsed TOML config for the :pep621:`project table <table-name>`.
:rtype:
.. versionchanged:: $VERSION
Extra names with hyphens are now considered valid.
If two extra names would normalize to the same string per :pep:`685` a warning is emitted.
In a future version this will become an error.
.. attention::
A future version of `pyproject-parser` will normalize all extra names
and write them to ``METADATA`` in this normalized form.
"""

parsed_optional_dependencies: Dict[str, Set[ComparableRequirement]] = dict()

err_template = (
f"Invalid type for 'project.optional-dependencies{{idx_string}}': "
f"expected {dict!r}, got {{actual_type!r}}"
)
normalized_names: Set[str] = set() # remove for part 2

optional_dependencies: Mapping[str, Any] = config["optional-dependencies"]

if not isinstance(optional_dependencies, dict):
raise TypeError(err_template.format(idx_string='', actual_type=type(optional_dependencies)))
raise TypeError(
"Invalid type for 'project.optional-dependencies': "
f"expected {dict!r}, got {type(optional_dependencies)!r}",
)

for extra, dependencies in optional_dependencies.items():
if not extra.isidentifier():
raise TypeError(f"Invalid extra name {extra!r}: must be a valid Python identifier")

self.assert_sequence_not_str(dependencies, path=["project", "optional-dependencies", extra])
# Normalize per PEP 685
normalized_extra = normalize(extra)

path = ("project", "optional-dependencies", extra)

if normalized_extra in normalized_names: # parsed_optional_dependencies for part 2
warnings.warn(
f"{construct_path(path)!r}: "
f"Multiple extras were defined with the same normalized name of {normalized_extra!r}",
PyProjectDeprecationWarning,
)
# For part 2
# raise BadConfigError(
# f"{construct_path(path)!r}: "
# f"Multiple extras were defined with the same normalized name of {normalized_extra!r}",
# )

parsed_optional_dependencies[extra] = set()
# https://packaging.python.org/specifications/core-metadata/#provides-extra-multiple-use
# if not extra_re.match(normalized_extra):
if not (extra.isidentifier() or extra_re.match(normalized_extra)):
raise TypeError(f"Invalid extra name {extra!r} ({extra_re.match(normalized_extra)})")

self.assert_sequence_not_str(dependencies, path=path)

parsed_optional_dependencies[extra] = set() # normalized_extra for part 2
normalized_names.add(normalized_extra) # Remove for part 2

for idx, dep in enumerate(dependencies):
if isinstance(dep, str):
# normalized_extra for part 2
parsed_optional_dependencies[extra].add(ComparableRequirement(dep))
else:
raise TypeError(err_template.format(idx_string=f'.{extra}[{idx}]', actual_type=type(dep)))
raise TypeError(
f"Invalid type for 'project.optional-dependencies.{extra}[{idx}]': "
f"expected {str!r}, got {type(dep)!r}"
)

return {e: sorted(combine_requirements(d)) for e, d in parsed_optional_dependencies.items()}

Expand Down
14 changes: 13 additions & 1 deletion pyproject_parser/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
# this package
from pyproject_parser.type_hints import ContentTypes

__all__ = ["render_markdown", "render_rst", "content_type_from_filename"]
__all__ = ["render_markdown", "render_rst", "content_type_from_filename", "PyProjectDeprecationWarning"]


def render_markdown(content: str) -> None:
Expand Down Expand Up @@ -137,3 +137,15 @@ def render_readme(
render_markdown(content)
elif content_type == "text/x-rst":
render_rst(content)


class PyProjectDeprecationWarning(Warning):
"""
Warning for the use of deprecated features in `pyproject.toml`.
This is a user-facing warning which will be shown by default.
For developer-facing warnings intended for direct consumers of this library,
use a standard :class:`DeprecationWarning`.
.. versionadded:: $VERSION
"""
3 changes: 3 additions & 0 deletions repo_helper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ intersphinx_mapping:

exclude_files:
- contributing

tox_unmanaged:
- coverage:report
75 changes: 75 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# stdlib
import json
import re
import subprocess
import warnings
from typing import Optional

# 3rd party
Expand Down Expand Up @@ -81,6 +83,79 @@ def test_check(
assert result.stdout == "Validating 'pyproject.toml'\n"


@pytest.mark.parametrize(
"toml_string",
[
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev_test" = []\n"dev-test" = []',
id="duplicate_extra_1",
),
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev-test" = []\n"dev_test" = []',
id="duplicate_extra_2",
),
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev.test" = []\n"dev_test" = []',
id="duplicate_extra_3",
),
]
)
def test_check_extra_deprecation(
toml_string: str,
tmp_pathplus: PathPlus,
cli_runner: CliRunner,
advanced_file_regression: AdvancedFileRegressionFixture,
):
(tmp_pathplus / "pyproject.toml").write_clean(toml_string)
cli_runner.mix_stderr = False

with in_directory(tmp_pathplus), warnings.catch_warnings():
warnings.simplefilter("error")
result: Result = cli_runner.invoke(check, catch_exceptions=False)

assert result.exit_code == 1
assert result.stdout == "Validating 'pyproject.toml'\n"
advanced_file_regression.check(result.stderr)


@pytest.mark.parametrize(
"toml_string",
[
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev_test" = []\n"dev-test" = []',
id="duplicate_extra_1",
),
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev-test" = []\n"dev_test" = []',
id="duplicate_extra_2",
),
pytest.param(
'[project]\nname = "foo"\nversion = "1.2.3"\n[project.optional-dependencies]\n"dev.test" = []\n"dev_test" = []',
id="duplicate_extra_3",
),
]
)
def test_check_extra_deprecation_warning(
toml_string: str,
tmp_pathplus: PathPlus,
cli_runner: CliRunner,
advanced_file_regression: AdvancedFileRegressionFixture,
):
(tmp_pathplus / "pyproject.toml").write_clean(toml_string)

args = ["pyproject-parser", "check"]

with in_directory(tmp_pathplus):
process = subprocess.run(
args,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
)
assert process.returncode == 0

advanced_file_regression.check(process.stdout.decode("UTF-8"))


@pytest.mark.parametrize(
"toml_string, match",
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
An error occurred: 'project.optional-dependencies.dev-test': Multiple extras were defined with the same normalized name of 'dev-test'
Aborted!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
An error occurred: 'project.optional-dependencies.dev_test': Multiple extras were defined with the same normalized name of 'dev-test'
Aborted!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
An error occurred: 'project.optional-dependencies.dev_test': Multiple extras were defined with the same normalized name of 'dev-test'
Aborted!
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Validating 'pyproject.toml'
WARNING: 'project.optional-dependencies.dev-test': Multiple extras were defined with the same normalized name of 'dev-test'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Validating 'pyproject.toml'
WARNING: 'project.optional-dependencies.dev_test': Multiple extras were defined with the same normalized name of 'dev-test'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Validating 'pyproject.toml'
WARNING: 'project.optional-dependencies.dev_test': Multiple extras were defined with the same normalized name of 'dev-test'
14 changes: 13 additions & 1 deletion tests/test_cli_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import email.headerregistry
import re
import sys
import warnings

# 3rd party
import click
import pytest
from consolekit.testing import CliRunner, Result
from consolekit.tracebacks import handle_tracebacks
from dom_toml.parser import BadConfigError
from domdf_python_tools.utils import redirect_output

# this package
from pyproject_parser.cli import ConfigTracebackHandler, resolve_class
from pyproject_parser.cli import ConfigTracebackHandler, prettify_deprecation_warning, resolve_class
from pyproject_parser.utils import PyProjectDeprecationWarning

exceptions = pytest.mark.parametrize(
"exception",
Expand Down Expand Up @@ -105,3 +108,12 @@ def test_resolve_class():
resolve_class("collections.Counter", "class")

assert e.value.option_name == "class"


def test_prettify_deprecation_warning():

with redirect_output() as (stdout, stderr):
prettify_deprecation_warning()
warnings.warn("This is a warning", PyProjectDeprecationWarning)

assert stderr.getvalue() == 'WARNING: This is a warning\n'

0 comments on commit d98192d

Please sign in to comment.