Skip to content
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
13 changes: 13 additions & 0 deletions src/specify_cli/bundler/lib/versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ def _normalize_constraint(value: str) -> str:
if not raw.strip():
continue
match = _SPECIFIER_CLAUSE.match(raw)
if match is None:
# ``_SPECIFIER_CLAUSE`` is anchored with ``^``/``$`` and ``.`` does
# not cross newlines, so a clause containing an EMBEDDED newline
# does not match at all and ``match.groups()`` raised a raw
# AttributeError -- escaping ``parse_constraint``'s contract to
# report bad input as a BundlerError. A YAML block literal is an
# ordinary way to reach this:
# requires:
# speckit_version: |
# >=1.0.0
# <2.0.0
# which loads as ">=1.0.0\n<2.0.0\n".
raise InvalidSpecifier(f"Invalid specifier: {raw!r}")
operator, version = match.groups()
clauses.append(f"{operator or ''}{_normalize_semver(version)}")
return ",".join(clauses)
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/test_bundler_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,49 @@ def test_parse_constraint_empty_is_permissive():
from specify_cli.bundler.lib.versioning import parse_constraint

assert str(parse_constraint("")) == ""


@pytest.mark.parametrize(
"constraint",
[
">=1.0.0\n<2.0.0\n", # a YAML block literal, as loaded
">=1.0\n.0",
"a\nb",
],
ids=["yaml_block_literal", "split_version", "garbage"],
)
def test_constraint_with_an_embedded_newline_reports_a_bundler_error(constraint):
"""A clause containing a newline must be reported, not crash.

`_SPECIFIER_CLAUSE` is anchored with `^`/`$` and `.` does not cross
newlines, so such a clause does not match at all and `match.groups()`
raised a raw `AttributeError` — escaping `parse_constraint`'s contract to
surface bad input as a `BundlerError`. A YAML block literal reaches this
with no exotic input at all:

requires:
speckit_version: |
>=1.0.0
<2.0.0
"""
from specify_cli.bundler.lib.versioning import parse_constraint

with pytest.raises(BundlerError, match="Invalid version constraint"):
parse_constraint(constraint)


@pytest.mark.parametrize(
"constraint,expected",
[
(">=1.0.0", ">=1.0.0"),
(">=v1.0.0", ">=1.0.0"),
("~=1.2", "~=1.2"),
(">=1.0.0\n", ">=1.0.0"), # trailing newline is still stripped
("\n>=1.0.0", ">=1.0.0"), # leading newline too
],
)
def test_valid_constraints_are_unaffected(constraint, expected):
"""Only clauses that genuinely fail to match change behaviour."""
from specify_cli.bundler.lib.versioning import parse_constraint

assert str(parse_constraint(constraint)) == expected