Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Be nicer about possible misconfigurations #1004

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/setuptools_scm/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:

group_names = regex.groupindex.keys()
if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names):
warnings.warn(
"Expected tag_regex to contain a single match group or a group named"
" 'version' to identify the version part of any tag."
raise ValueError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is possibly a breaking change im not sure we can integrate it off hand

f"Expected tag_regex '{regex.pattern}' to contain a single match group or"
" a group named 'version' to identify the version part of any tag."
)

return regex
Expand Down Expand Up @@ -105,6 +105,9 @@ class Configuration:

parent: _t.PathT | None = None

def __post_init__(self) -> None:
self.tag_regex = _check_tag_regex(self.tag_regex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking this here without leaving it in place creates potental trouble for valies valid in config but not in the config objects


@property
def absolute_root(self) -> str:
return _check_absolute_root(self.root, self.relative_to)
Expand Down Expand Up @@ -139,13 +142,11 @@ def from_data(
given configuration data
create a config instance after validating tag regex/version class
"""
tag_regex = _check_tag_regex(data.pop("tag_regex", None))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check needs to say even if the validation gets more struct

version_cls = _validate_version_cls(
data.pop("version_cls", None), data.pop("normalize", True)
)
return cls(
relative_to,
version_cls=version_cls,
tag_regex=tag_regex,
**data,
)
37 changes: 14 additions & 23 deletions src/setuptools_scm/_entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from typing import Callable
from typing import cast
from typing import Iterator
from typing import overload
from typing import TYPE_CHECKING

from . import _log
Expand Down Expand Up @@ -106,34 +105,26 @@ def _iter_version_schemes(
yield scheme_value


@overload
def _call_version_scheme(
version: version.ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
default: str,
default: str | None = None,
) -> str:
...


@overload
def _call_version_scheme(
version: version.ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
default: None,
) -> str | None:
...


def _call_version_scheme(
version: version.ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
default: str | None,
) -> str | None:
found_any_implementation = False
for scheme in _iter_version_schemes(entrypoint, given_value):
found_any_implementation = True
result = scheme(version)
if result is not None:
return result
return default
if not found_any_implementation:
raise ValueError(
f'Couldn\'t find any implementations for entrypoint "{entrypoint}"'
f' with value "{given_value}".'
)
if default is not None:
return default
raise ValueError(
f'None of the "{entrypoint}" entrypoints matching "{given_value}"'
" returned a value."
)
25 changes: 16 additions & 9 deletions src/setuptools_scm/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,21 @@ def _parse_version_tag(
log.debug(
"key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
)
result = _TagDict(
version=match.group(key),
prefix=full[: match.start(key)],
suffix=full[match.end(key) :],
)

log.debug("tag %r parsed to %r", tag, result)
assert result["version"]
return result
if version := match.group(key):
result = _TagDict(
version=version,
prefix=full[: match.start(key)],
suffix=full[match.end(key) :],
)

log.debug("tag %r parsed to %r", tag, result)
return result

raise ValueError(
f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", '
"however the matched group has no value."
)
else:
log.debug("tag %r did not parse", tag)

Expand Down Expand Up @@ -428,8 +434,9 @@ def format_version(version: ScmVersion) -> str:
if version.preformatted:
assert isinstance(version.tag, str)
return version.tag

main_version = _entrypoints._call_version_scheme(
version, "setuptools_scm.version_scheme", version.config.version_scheme, None
version, "setuptools_scm.version_scheme", version.config.version_scheme
)
log.debug("version %s", main_version)
assert main_version is not None
Expand Down
20 changes: 20 additions & 0 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,23 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No

assert pristine.root != overridden.root
assert pristine.fallback_root != overridden.fallback_root


@pytest.mark.parametrize(
"tag_regex",
[
r".*",
r"(.+)(.+)",
r"((.*))",
],
)
def test_config_bad_regex(tag_regex: str) -> None:
with pytest.raises(
ValueError,
match=(
f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match"
" group or a group named 'version' to identify the version part of any"
" tag."
),
):
Configuration(tag_regex=re.compile(tag_regex))
46 changes: 46 additions & 0 deletions testing/test_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from dataclasses import replace
from datetime import date
from datetime import timedelta
Expand Down Expand Up @@ -189,6 +190,17 @@ def test_tag_regex1(tag: str, expected: str) -> None:
assert result.tag.public == expected


def test_regex_match_but_no_version() -> None:
with pytest.raises(
ValueError,
match=(
r'The tag_regex "\(\?P<version>\)\.\*" matched tag "v1",'
" however the matched group has no value"
),
):
meta("v1", config=replace(c, tag_regex=re.compile("(?P<version>).*")))


@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471")
def test_version_bump_bad() -> None:
class YikesVersion:
Expand Down Expand Up @@ -410,3 +422,37 @@ def __repr__(self) -> str:

assert isinstance(scm_version.tag, MyVersion)
assert str(scm_version.tag) == "Custom 1.0.0-foo"


@pytest.mark.parametrize("config_key", ["version_scheme", "local_scheme"])
def test_no_matching_entrypoints(config_key: str) -> None:
version = meta(
"1.0",
config=replace(c, **{config_key: "nonexistant"}), # type: ignore
)
with pytest.raises(
ValueError,
match=(
r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?"'
' with value "nonexistant"'
),
):
format_version(version)


def test_all_entrypoints_return_none() -> None:
version = meta(
"1.0",
config=replace(
c,
version_scheme=lambda v: None, # type: ignore
),
)
with pytest.raises(
ValueError,
match=(
'None of the "setuptools_scm.version_scheme" entrypoints matching'
r" .*? returned a value."
),
):
format_version(version)
Loading