Skip to content

Commit 3d312e1

Browse files
authored
Create an API for linting a file by path (#4163)
* Create an API for linting a file by path
1 parent 5833a31 commit 3d312e1

File tree

3 files changed

+155
-2
lines changed

3 files changed

+155
-2
lines changed

src/cfnlint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import logging
77

8-
from cfnlint.api import lint, lint_all
8+
from cfnlint.api import lint, lint_all, lint_file
99
from cfnlint.config import ConfigMixIn, ManualArgs
1010
from cfnlint.rules import Rules
1111
from cfnlint.template import Template

src/cfnlint/api.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
from pathlib import Path
89
from typing import List
910

1011
from cfnlint.config import ConfigMixIn, ManualArgs
@@ -82,3 +83,42 @@ def lint_all(s: str) -> list[Match]:
8283
include_checks=["I"], include_experimental=True, regions=REGIONS
8384
),
8485
)
86+
87+
88+
def lint_file(
89+
template: Path,
90+
config: ManualArgs | None = None,
91+
) -> list[Match]:
92+
"""Validate a template file using the configuration provided.
93+
94+
Parameters
95+
----------
96+
filename : str
97+
Path to the CloudFormation template file
98+
config : ManualArgs
99+
Configuration options for the linter
100+
101+
Returns
102+
-------
103+
list
104+
a list of errors if any were found, else an empty list
105+
"""
106+
107+
if not template.exists():
108+
from cfnlint.rules.errors import ParseError
109+
110+
return [
111+
Match.create(
112+
filename=str(template),
113+
rule=ParseError(),
114+
message=f"Template file not found: {str(template)}",
115+
)
116+
]
117+
118+
if not config:
119+
config_mixin = ConfigMixIn(["--template", str(template)])
120+
else:
121+
config_mixin = ConfigMixIn(["--template", str(template)], **config)
122+
123+
runner = Runner(config_mixin)
124+
return list(runner.run())

test/unit/module/test_api.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
from __future__ import annotations
77

8+
import os
9+
import tempfile
10+
from pathlib import Path
811
from unittest import TestCase
912

10-
from cfnlint import lint, lint_all
13+
from cfnlint import lint, lint_all, lint_file
1114
from cfnlint.config import ManualArgs
1215
from cfnlint.core import get_rules
1316
from cfnlint.helpers import REGIONS
@@ -167,3 +170,113 @@ def test_sam_template(self):
167170
filename = "test/fixtures/templates/good/transform/list_transform_many.yaml"
168171
matches = self.helper_lint_string_from_file(filename)
169172
self.assertEqual([], matches, f"Got matches: {matches!r}")
173+
174+
175+
class TestLintFile(TestCase):
176+
"""Test the lint_file API function"""
177+
178+
def test_nonexistent_file(self):
179+
"""Test linting a file that doesn't exist"""
180+
matches = lint_file(Path("nonexistent_file.yaml"))
181+
self.assertEqual(1, len(matches))
182+
self.assertEqual("E0000", matches[0].rule.id)
183+
self.assertIn("Template file not found", matches[0].message)
184+
185+
def test_noecho_yaml_template(self):
186+
"""Test linting a template with NoEcho issues"""
187+
filename = Path("test/fixtures/templates/bad/noecho.yaml")
188+
matches = lint_file(
189+
filename,
190+
config=ManualArgs(regions=["us-east-1", "us-west-2", "eu-west-1"]),
191+
)
192+
self.assertEqual(
193+
["W2010", "W2010"],
194+
[match.rule.id for match in matches],
195+
f"Got matches: {matches!r}",
196+
)
197+
198+
def test_noecho_yaml_template_warnings_ignored(self):
199+
"""Test linting with warnings ignored"""
200+
filename = Path("test/fixtures/templates/bad/noecho.yaml")
201+
matches = lint_file(
202+
filename,
203+
config=ManualArgs(
204+
ignore_checks=["W", "I"],
205+
),
206+
)
207+
self.assertListEqual([], matches, f"Got matches: {matches!r}")
208+
209+
def test_duplicate_json_template(self):
210+
"""Test linting a template with duplicate keys"""
211+
filename = Path("test/fixtures/templates/bad/duplicate.json")
212+
matches = lint_file(
213+
filename,
214+
config=ManualArgs(
215+
regions=["us-east-1", "us-west-2", "eu-west-1"],
216+
),
217+
)
218+
self.assertEqual(
219+
["E0000", "E0000", "E0000"],
220+
[match.rule.id for match in matches],
221+
f"Got matches: {matches!r}",
222+
)
223+
224+
def test_invalid_yaml_template(self):
225+
"""Test linting an invalid YAML template"""
226+
filename = Path("test/fixtures/templates/bad/core/config_invalid_yaml.yaml")
227+
matches = lint_file(
228+
filename,
229+
config=ManualArgs(regions=["us-east-1", "us-west-2", "eu-west-1"]),
230+
)
231+
self.assertEqual(
232+
["E0000"], [match.rule.id for match in matches], f"Got matches: {matches!r}"
233+
)
234+
235+
def test_invalid_json_template(self):
236+
"""Test linting an invalid JSON template"""
237+
filename = Path("test/fixtures/templates/bad/core/config_invalid_json.json")
238+
matches = lint_file(
239+
filename,
240+
config=ManualArgs(regions=["us-east-1", "us-west-2", "eu-west-1"]),
241+
)
242+
self.assertEqual(
243+
["E0000"], [match.rule.id for match in matches], f"Got matches: {matches!r}"
244+
)
245+
246+
def test_issues_template(self):
247+
"""Test linting a template with issues"""
248+
filename = Path("test/fixtures/templates/bad/issues.yaml")
249+
matches = lint_file(
250+
filename,
251+
config=ManualArgs(regions=["us-east-1", "us-west-2", "eu-west-1"]),
252+
)
253+
self.assertEqual(
254+
["E1020"], [match.rule.id for match in matches], f"Got matches: {matches!r}"
255+
)
256+
257+
def test_sam_template(self):
258+
"""Test linting a SAM template"""
259+
filename = Path(
260+
"test/fixtures/templates/good/transform/list_transform_many.yaml"
261+
)
262+
matches = lint_file(filename)
263+
self.assertEqual([], matches, f"Got matches: {matches!r}")
264+
265+
def test_empty_file(self):
266+
"""Test linting an empty file"""
267+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
268+
temp_file.write(b"")
269+
temp_file_path = Path(temp_file.name)
270+
271+
try:
272+
matches = lint_file(temp_file_path)
273+
self.assertEqual(1, len(matches))
274+
self.assertEqual("E1001", matches[0].rule.id)
275+
finally:
276+
os.unlink(temp_file_path)
277+
278+
def test_good_template(self):
279+
"""Test linting a good template"""
280+
filename = Path("test/fixtures/templates/good/generic.yaml")
281+
matches = lint_file(filename)
282+
self.assertEqual([], matches, f"Got matches: {matches!r}")

0 commit comments

Comments
 (0)