Skip to content

Commit

Permalink
feat: allow numbers in component names (#4945)
Browse files Browse the repository at this point in the history
- Allow for digits in component names.
- Refactor to use same validation for snap and component names.
- Begin snap and component names errors with a lowercase character.

---------

Signed-off-by: Callahan Kovacs <[email protected]>
  • Loading branch information
mr-cal authored Aug 6, 2024
1 parent 3cdb859 commit 8ab7fd0
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 54 deletions.
63 changes: 33 additions & 30 deletions snapcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,26 +238,44 @@ def _validate_version_name(version: str, model_name: str) -> None:
)


def _validate_component_name(name: str) -> None:
"""Validate a component name."""
if not re.fullmatch(r"[a-z-]*[a-z][a-z-]*", name):
raise ValueError(
"Component names can only use ASCII lowercase letters and hyphens"
)
def _validate_name(*, name: str, field_name: str) -> str:
"""Validate a name.
if name.startswith("snap-"):
:param name: The name to validate.
:param field_name: The name of the field being validated.
:returns: The validated name.
"""
if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name):
raise ValueError(
"Component names cannot start with the reserved namespace 'snap-'"
f"{field_name} names can only use lowercase alphanumeric "
"and hyphens and must have at least one letter"
)

if name.startswith("-"):
raise ValueError("Component names cannot start with a hyphen")
raise ValueError(f"{field_name} names cannot start with a hyphen")

if name.endswith("-"):
raise ValueError("Component names cannot end with a hyphen")
raise ValueError(f"{field_name} names cannot end with a hyphen")

if "--" in name:
raise ValueError("Component names cannot have two hyphens in a row")
raise ValueError(f"{field_name} names cannot have two hyphens in a row")

return name


def _validate_component(name: str) -> str:
"""Validate a component name.
:param name: The component name to validate.
:returns: The validated component name.
"""
if name.startswith("snap-"):
raise ValueError(
"component names cannot start with the reserved prefix 'snap-'"
)
return _validate_name(name=name, field_name="component")


def _get_partitions_from_components(
Expand Down Expand Up @@ -731,23 +749,8 @@ def _validate_mandatory_base(cls, values):

@pydantic.validator("name")
@classmethod
def _validate_name(cls, name):
if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name):
raise ValueError(
"Snap names can only use ASCII lowercase letters, numbers, and hyphens, "
"and must have at least one letter"
)

if name.startswith("-"):
raise ValueError("Snap names cannot start with a hyphen")

if name.endswith("-"):
raise ValueError("Snap names cannot end with a hyphen")

if "--" in name:
raise ValueError("Snap names cannot have two hyphens in a row")

return name
def _validate_snap_name(cls, name):
return _validate_name(name=name, field_name="snap")

@pydantic.validator("version")
@classmethod
Expand All @@ -764,7 +767,7 @@ def _validate_version(cls, version, values):
def _validate_components(cls, components):
"""Validate component names."""
for component_name in components.keys():
_validate_component_name(component_name)
_validate_component(name=component_name)

return components

Expand Down Expand Up @@ -1077,7 +1080,7 @@ class ComponentProject(models.CraftBaseModel, extra=pydantic.Extra.ignore):
def _validate_components(cls, components):
"""Validate component names."""
for component_name in components.keys():
_validate_component_name(component_name)
_validate_component(name=component_name)

return components

Expand Down
51 changes: 27 additions & 24 deletions tests/unit/models/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,13 @@ def test_project_name_valid(self, name, project_yaml_data):
@pytest.mark.parametrize(
"name,error",
[
("name_with_underscores", "Snap names can only use"),
("name-with-UPPERCASE", "Snap names can only use"),
("name with spaces", "Snap names can only use"),
("-name-starts-with-hyphen", "Snap names cannot start with a hyphen"),
("name-ends-with-hyphen-", "Snap names cannot end with a hyphen"),
("name-has--two-hyphens", "Snap names cannot have two hyphens in a row"),
("123456", "Snap names can only use"),
("name_with_underscores", "snap names can only use"),
("name-with-UPPERCASE", "snap names can only use"),
("name with spaces", "snap names can only use"),
("-name-starts-with-hyphen", "snap names cannot start with a hyphen"),
("name-ends-with-hyphen-", "snap names cannot end with a hyphen"),
("name-has--two-hyphens", "snap names cannot have two hyphens in a row"),
("123456", "snap names can only use"),
(
"a2345678901234567890123456789012345678901",
"ensure this value has at most 40 characters",
Expand Down Expand Up @@ -2490,7 +2490,15 @@ def test_component_type_invalid(
project.unmarshal(project_yaml_data(components=component))

@pytest.mark.parametrize(
"name", ["name", "name-with-dashes", "x" * 40, "foo-snap-bar"]
"name",
[
"name",
"name-with-dashes",
"name-with-numbers-0123",
"0123-name-with-numbers",
"x" * 40,
"foo-snap-bar",
],
)
def test_component_name_valid(
self, project, name, project_yaml_data, stub_component_data
Expand All @@ -2505,26 +2513,21 @@ def test_component_name_valid(
@pytest.mark.parametrize(
"name,error",
[
(
"snap-",
"Component names cannot start with the reserved namespace 'snap-'",
),
(
pytest.param(
"snap-foo",
"Component names cannot start with the reserved namespace 'snap-'",
"component names cannot start with the reserved prefix 'snap-'",
id="reserved prefix",
),
("123456", "Component names can only use"),
("name-ends-with-digits-0123", "Component names can only use"),
("456-name-starts-with-digits", "Component names can only use"),
("name-789-contains-digits", "Component names can only use"),
("name_with_underscores", "Component names can only use"),
("name-with-UPPERCASE", "Component names can only use"),
("name with spaces", "Component names can only use"),
("-name-starts-with-hyphen", "Component names cannot start with a hyphen"),
("name-ends-with-hyphen-", "Component names cannot end with a hyphen"),
pytest.param("123456", "component names can only use", id="no letters"),
("name_with_underscores", "component names can only use"),
("name-with-UPPERCASE", "component names can only use"),
("name with spaces", "component names can only use"),
("name-with-$symbols", "component names can only use"),
("-name-starts-with-hyphen", "component names cannot start with a hyphen"),
("name-ends-with-hyphen-", "component names cannot end with a hyphen"),
(
"name-has--two-hyphens",
"Component names cannot have two hyphens in a row",
"component names cannot have two hyphens in a row",
),
("x" * 41, "ensure this value has at most 40 characters"),
],
Expand Down

0 comments on commit 8ab7fd0

Please sign in to comment.