Skip to content

Commit 8d1589a

Browse files
authored
Create API for just taking a config (#4177)
* Create API for just taking a config
1 parent 96277d5 commit 8d1589a

File tree

10 files changed

+600
-27
lines changed

10 files changed

+600
-27
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, lint_file
8+
from cfnlint.api import lint, lint_all, lint_by_config, 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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,37 @@ def lint_file(
127127

128128
runner = Runner(config_mixin)
129129
return list(runner.run())
130+
131+
132+
def lint_by_config(config: ManualArgs) -> list[Match]:
133+
"""Validate a template using a Config
134+
135+
Parameters
136+
----------
137+
config : ManualArgs
138+
Configuration options for the linter
139+
140+
Returns
141+
-------
142+
list
143+
a list of errors if any were found, else an empty list
144+
"""
145+
146+
config_mixin = ConfigMixIn(**config)
147+
148+
# Use the centralized validation logic
149+
try:
150+
config_mixin.validate()
151+
except ValueError as e:
152+
from cfnlint.rules.errors import ConfigError
153+
154+
return [
155+
Match.create(
156+
message=str(e),
157+
filename="",
158+
rule=ConfigError(),
159+
)
160+
]
161+
162+
runner = Runner(config_mixin)
163+
return list(runner.run())

src/cfnlint/config.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,6 @@ def _merge_configs(
731731

732732
# pylint: disable=too-many-public-methods
733733
class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs):
734-
735734
def __init__(self, cli_args: list[str] | None = None, **kwargs: Unpack[ManualArgs]):
736735
self._manual_args = kwargs or ManualArgs()
737736
CliArgs.__init__(self, cli_args)
@@ -793,6 +792,68 @@ def __eq__(self, value):
793792

794793
return True
795794

795+
def validate(self, allow_stdin: bool = False) -> None:
796+
"""
797+
Validate the configuration for logical consistency.
798+
799+
This method validates configuration constraints that may not be enforced
800+
by argparse when using the API directly (vs CLI usage). While the CLI
801+
uses argparse mutually exclusive groups to prevent some conflicts, the
802+
API bypasses argparse, so this method ensures all constraints are enforced
803+
consistently across both usage patterns.
804+
805+
Args:
806+
allow_stdin: If True, allows validation to pass when no templates/deployment
807+
files are specified (for CLI stdin handling)
808+
809+
Raises:
810+
ValueError: When configuration is invalid with a descriptive message
811+
"""
812+
# Get raw configuration values using the same logic as the templates property
813+
# For templates, we need to check both templates and template_alt
814+
raw_templates = []
815+
if "templates" in self._manual_args:
816+
raw_templates = self._manual_args["templates"]
817+
else:
818+
cli_alt_args = self._get_argument_value("template_alt", False, False)
819+
cli_args = self._get_argument_value("templates", False, False)
820+
if cli_alt_args:
821+
raw_templates = cli_alt_args
822+
elif cli_args:
823+
raw_templates = cli_args
824+
825+
# Ensure it's a list
826+
if isinstance(raw_templates, str):
827+
raw_templates = [raw_templates]
828+
raw_templates = raw_templates or []
829+
raw_deployment_files = (
830+
self._get_argument_value("deployment_files", False, False) or []
831+
)
832+
raw_parameters = self._get_argument_value("parameters", False, False) or {}
833+
raw_parameter_files = (
834+
self._get_argument_value("parameter_files", False, False) or []
835+
)
836+
837+
# Check if no templates or deployment files are specified
838+
if not raw_templates and not raw_deployment_files and not allow_stdin:
839+
raise ValueError("No templates or deployment files specified")
840+
841+
# Check for conflicting deployment files with other options
842+
if raw_deployment_files:
843+
if raw_templates or raw_parameters or raw_parameter_files:
844+
raise ValueError(
845+
"Deployment files cannot be used with templates, parameters, "
846+
"or parameter files"
847+
)
848+
849+
# Check for conflicting parameter options
850+
if raw_parameters and raw_parameter_files:
851+
raise ValueError("Cannot specify both --parameters and --parameter-files")
852+
853+
# Check for multiple templates with parameters
854+
if (raw_parameters or raw_parameter_files) and len(raw_templates) > 1:
855+
raise ValueError("Parameters can only be used with a single template")
856+
796857
def _get_argument_value(self, arg_name, is_template, is_config_file):
797858
cli_value = getattr(self.cli_args, arg_name)
798859
template_value = self.template_args.get(arg_name)
@@ -1032,7 +1093,6 @@ def force(self):
10321093
return self._get_argument_value("force", False, False)
10331094

10341095
def evolve(self, **kwargs: Unpack[ManualArgs]) -> "ConfigMixIn":
1035-
10361096
config = deepcopy(self)
10371097
config._manual_args.update(kwargs)
10381098
return config

src/cfnlint/rules/jsonschema/Base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from cfnlint.context import Context
1212
from cfnlint.jsonschema import V, ValidationError, Validator
13+
from cfnlint.jsonschema.exceptions import _unset
1314
from cfnlint.rules import CloudFormationLintRule, RuleMatch
1415
from cfnlint.schema.resolver import RefResolver
1516

@@ -33,8 +34,13 @@ def _convert_validation_errors_to_matches(
3334
):
3435
matches = []
3536
kwargs: dict[Any, Any] = {}
37+
38+
# Only add validator and instance if they are not unset
39+
if e.validator is not _unset:
40+
kwargs["validator"] = e.validator
41+
3642
if e.extra_args:
37-
kwargs = e.extra_args
43+
kwargs.update(e.extra_args)
3844
e_path = list(path) + list(e.path)
3945
if len(e.path) > 0:
4046
e_path_override = e.path_override

src/cfnlint/runner/cli.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -285,26 +285,22 @@ def cli(self) -> None:
285285
print(self.rules)
286286
sys.exit(0)
287287

288-
if not self.config.templates and not self.config.deployment_files:
289-
if sys.stdin.isatty():
290-
self.config.parser.print_help()
291-
sys.exit(1)
292-
293-
if self.config.deployment_files:
294-
if (
295-
self.config.templates
296-
or self.config.parameters
297-
or self.config.parameter_files
298-
):
299-
self.config.parser.print_help()
300-
sys.exit(1)
301-
302-
if self.config.parameters and self.config.parameter_files:
288+
# Use centralized configuration validation
289+
# For CLI, we allow stdin input when no templates/deployment files are specified
290+
try:
291+
self.config.validate(allow_stdin=True)
292+
except ValueError as e:
293+
print(f"Configuration error: {e}")
303294
self.config.parser.print_help()
304295
sys.exit(1)
305296

306-
if self.config.parameters or self.config.parameter_files:
307-
if len(self.config.templates) > 1:
297+
# Special case: if no templates/deployment files and stdin is a tty, show help
298+
raw_templates = self.config._get_argument_value("templates", True, False) or []
299+
raw_deployment_files = (
300+
self.config._get_argument_value("deployment_files", False, False) or []
301+
)
302+
if not raw_templates and not raw_deployment_files:
303+
if sys.stdin.isatty():
308304
self.config.parser.print_help()
309305
sys.exit(1)
310306

src/cfnlint/runner/deployment_file/deployment_types/git_sync.py

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

1616

1717
def create_deployment_from_git_sync(
18-
data: dict[str, Any]
18+
data: dict[str, Any],
1919
) -> tuple[DeploymentFileData | None, RuleMatches | None]:
2020

2121
schema = load_resource(cfnlint.data.schemas.other.deployment_files, "git_sync.json")

0 commit comments

Comments
 (0)