fix(sdk): honor from_cli_args return value in init_global_provider fallback

This commit is contained in:
StylusFrost
2026-04-24 18:51:57 +02:00
parent 60e7657081
commit cf70d1f9f8
2 changed files with 78 additions and 3 deletions
+18 -3
View File
@@ -146,7 +146,13 @@ class Provider(ABC):
@classmethod
def from_cli_args(cls, arguments: Namespace, fixer_config: dict) -> "Provider":
"""Instantiate the provider from CLI arguments."""
"""Instantiate the provider from CLI arguments and return the instance.
The caller wires the returned instance into the global provider slot
via Provider.set_global_provider(). Implementations that already call
set_global_provider(self) from __init__ are also supported — the call
site tolerates a None return in that case.
"""
raise NotImplementedError(f"{cls.__name__} has not implemented from_cli_args()")
def get_output_options(self, arguments, bulk_checks_metadata):
@@ -507,8 +513,17 @@ class Provider(ABC):
fixer_config=fixer_config,
)
else:
# Dynamic fallback: any external/custom provider
provider_class.from_cli_args(arguments, fixer_config)
# Dynamic fallback: any external/custom provider.
# Honor the from_cli_args type hint (-> Provider): if the
# implementation returns an instance, wire it as the global
# provider here. Implementations that call
# set_global_provider(self) from __init__ return None and
# remain supported (the condition below is a no-op for them).
provider_instance = provider_class.from_cli_args(
arguments, fixer_config
)
if provider_instance is not None:
Provider.set_global_provider(provider_instance)
except TypeError as error:
logger.critical(
@@ -145,6 +145,41 @@ class FakeToolWrapperProvider(Provider):
pass
class FakePureContractProvider(Provider):
"""External provider that honors the from_cli_args type hint literally:
returns an instance without calling Provider.set_global_provider() from
__init__. Used to verify the call site wires the returned instance."""
_type = "fakepure"
@property
def type(self):
return self._type
@property
def identity(self):
return MagicMock(host_id="fake-pure-1")
@property
def session(self):
return MagicMock()
@property
def audit_config(self):
return {}
def setup_session(self):
return MagicMock()
def print_credentials(self):
pass
@classmethod
def from_cli_args(cls, arguments, fixer_config):
# Literal contract: return the instance, no side-effect in __init__.
return cls()
class FakeProviderNoHelpText(Provider):
"""Provider without _cli_help_text."""
@@ -439,6 +474,31 @@ class TestProviderInitialization:
with pytest.raises(SystemExit):
Provider.init_global_provider(args)
@patch("prowler.providers.common.provider.load_and_validate_config_file")
@patch("prowler.providers.common.provider.Provider._load_ep_provider")
@patch("prowler.providers.common.provider.import_module")
def test_init_global_provider_wires_instance_returned_by_from_cli_args(
self, mock_import, mock_load_ep, mock_config
):
"""A provider that implements from_cli_args as a pure function (returns
the instance without calling set_global_provider from __init__) is
correctly wired as the global provider by init_global_provider."""
mock_import.side_effect = ImportError("No built-in")
mock_load_ep.return_value = FakePureContractProvider
mock_config.return_value = {}
args = Namespace(
provider="fakepure",
fixer_config="config.yaml",
config_file="config.yaml",
)
Provider._global = None
Provider.init_global_provider(args)
assert isinstance(Provider._global, FakePureContractProvider)
Provider._global = None
# ===========================================================================
# 3. Check Discovery