Repository: canonical/snapcraft (language: python)

Issue:
snapcraft has more strict linting unexpectedly 
### Bug Description

When building a snap using at least revision 12523 to build snaps which built successfully with revisions like 12503 (latest/edge versus 8.x/edge), snapcraft will now emit some error messages.

### To Reproduce

Install snapcraft from latest/edge, build the snap with `snapcraft`.

### Environment

Ubuntu 23.10, building with `snapcraft --use-lxd`.

### snapcraft.yaml

```yaml
name: my-snap-name # you probably want to 'snapcraft register <name>'
base: core24 # the base snap is the execution environment for this snap
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
  This is my-snap's description. You have a paragraph or two to tell the
  most important story about your snap. Keep it under 100 words though,
  we live in tweetspace and your description wants to look good in the snap
  store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

apps:
  my-app:
    command: bash
    environment:
      foo: 1
```


### Relevant log output

```shell
dilyn@Ares:~/test -> snapcraft 
Starting snapcraft, version 8.3.2.post71+git3508cf39
Logging execution to '/home/dilyn/.local/state/snapcraft/log/snapcraft-20240905-191836.634265.log'
Bad snapcraft.yaml content:
- input should be a valid string (in field 'apps.my-app.environment.foo')
```


### Additional context

Quoting `1` results in successful builds (sans an error message unrelated to this issue).


Discussion / hints:
I think this has roughly the same cause as https://github.com/canonical/snapcraft/issues/4978
Makes sense -- all the revisions I have where this is working are before the pydantic 2 shift.

We can close as duplicate.
Let's leave this open. Even if the fixes are entirely upstream, we should use this issue to add regression tests in snapcraft.

Thank you for reporting us your feedback!

The internal ticket has been created: https://warthogs.atlassian.net/browse/CRAFT-3329.

> This message was autogenerated

I got the [`craft-parts` fix](https://github.com/canonical/craft-parts/pull/816) into snapcraft 8.4.0 but will need to get the [`craft-application` fix](https://github.com/canonical/craft-application/pull/450) in 8.4.1.
We will need a fix in craft-grammar too: https://github.com/canonical/craft-grammar/issues/49

Fix patch:
diff --git a/requirements-devel.txt b/requirements-devel.txt
index 3db5bdf4ef..01712df5c4 100644
--- a/requirements-devel.txt
+++ b/requirements-devel.txt
@@ -27,11 +27,11 @@ coverage==7.6.1
 craft-application==4.2.3
 craft-archives==2.0.0
 craft-cli==2.7.0
-craft-grammar==2.0.0
+craft-grammar==2.0.1
 craft-parts==2.1.1
 craft-platforms==0.1.1
 craft-providers==2.0.1
-craft-store==3.0.0
+craft-store==3.0.1
 cryptography==43.0.1
 cssutils==2.11.1
 dict2css==0.3.0.post1
diff --git a/requirements-docs.txt b/requirements-docs.txt
index ae23b0c27f..7e58aec50a 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -22,11 +22,11 @@ colorama==0.4.6
 craft-application==4.2.3
 craft-archives==2.0.0
 craft-cli==2.7.0
-craft-grammar==2.0.0
+craft-grammar==2.0.1
 craft-parts==2.1.1
 craft-platforms==0.1.1
 craft-providers==2.0.1
-craft-store==3.0.0
+craft-store==3.0.1
 cryptography==43.0.1
 cssutils==2.11.1
 dict2css==0.3.0.post1
diff --git a/requirements.txt b/requirements.txt
index fab86ab254..33eb42e01b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,11 +10,11 @@ click==8.1.7
 craft-application==4.2.3
 craft-archives==2.0.0
 craft-cli==2.7.0
-craft-grammar==2.0.0
+craft-grammar==2.0.1
 craft-parts==2.1.1
 craft-platforms==0.1.1
 craft-providers==2.0.1
-craft-store==3.0.0
+craft-store==3.0.1
 cryptography==43.0.1
 distro==1.9.0
 docutils==0.19
diff --git a/setup.py b/setup.py
index 7b2a06c73e..c1ed03edc2 100755
--- a/setup.py
+++ b/setup.py
@@ -101,11 +101,11 @@ def recursive_data_files(directory, install_directory):
     "craft-application~=4.1",
     "craft-archives~=2.0",
     "craft-cli~=2.6",
-    "craft-grammar~=2.0",
+    "craft-grammar>=2.0.1,<3.0.0",
     "craft-parts~=2.1",
     "craft-platforms~=0.1",
     "craft-providers~=2.0",
-    "craft-store~=3.0",
+    "craft-store>=3.0.1,<4.0.0",
     "docutils<0.20",  # Frozen until we can update sphinx dependencies.
     "gnupg",
     "jsonschema==2.5.1",


Test patch:
diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py
index 50b5b07adb..99ccc4c4ee 100644
--- a/tests/unit/models/test_projects.py
+++ b/tests/unit/models/test_projects.py
@@ -82,6 +82,44 @@ def _socket_yaml_data(**kwargs) -> Dict[str, Any]:
     yield _socket_yaml_data
 
 
+@pytest.fixture
+def fake_project_with_numbers(project_yaml_data):
+    """Returns a fake project with numbers in string fields.
+
+    This includes numbers in fields that are validated by snapcraft and fields
+    validated by craft-parts.
+    """
+    return project_yaml_data(
+        # string
+        version=1.0,
+        # string
+        icon=2,
+        # list[str]
+        website=[3.0, 4],
+        # dict[str, str]
+        environment={
+            "float": 5.0,
+            "int": 6,
+        },
+        parts={
+            "p1": {
+                "plugin": "nil",
+                # string
+                "source-type": 7,
+                # string
+                "source-commit": 8.0,
+                # list[str]
+                "build-snaps": [9, 10.0],
+                # dict[str, str]
+                "build-environment": [
+                    {"float": 11.0},
+                    {"int": 12},
+                ],
+            }
+        },
+    )
+
+
 class TestProjectDefaults:
     """Ensure unspecified items have the correct default value."""
 
@@ -701,6 +739,23 @@ def test_links_list(self, project_yaml_data):
             "https://github.com/NickvisionApps/Denaro",
         ]
 
+    def test_coerce_numbers(self, fake_project_with_numbers):
+        """Coerce numbers into strings."""
+        project = Project.unmarshal(fake_project_with_numbers)
+
+        assert project.version == "1.0"
+        assert project.icon == "2"
+        assert project.website == ["3.0", "4"]
+        assert project.environment == {"float": "5.0", "int": "6"}
+        # parts remain a dictionary with original types
+        assert project.parts["p1"]["source-type"] == 7
+        assert project.parts["p1"]["source-commit"] == 8.0
+        assert project.parts["p1"]["build-snaps"] == [9, 10.0]
+        assert project.parts["p1"]["build-environment"] == [
+            {"float": 11.0},
+            {"int": 12},
+        ]
+
 
 class TestHookValidation:
     """Validate hooks."""
@@ -1504,6 +1559,10 @@ def test_grammar_try(self, project_yaml_data):
         with pytest.raises(errors.ProjectValidationError, match=error):
             GrammarAwareProject.validate_grammar(data)
 
+    def test_grammar_number_coercion(self, fake_project_with_numbers):
+        """Ensure that grammar validation does not fail when coercing numbers into strings."""
+        GrammarAwareProject.validate_grammar(fake_project_with_numbers)
+
     def test_grammar_type_error(self, project_yaml_data):
         data = project_yaml_data(
             parts={
@@ -1516,7 +1575,7 @@ def test_grammar_type_error(self, project_yaml_data):
             }
         )
 
-        error = r"value must be a str: \[25\]"
+        error = r"Input should be a valid string \(in field 'parts\.p1\.source\[0\]'\)"
         with pytest.raises(errors.ProjectValidationError, match=error):
             GrammarAwareProject.validate_grammar(data)