diff --git a/RELEASE.md b/RELEASE.md index 22946bb30a..7451dda132 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -16,6 +16,7 @@ ## Bug fixes and other changes * Updated dataset factories to resolve nested catalog config properly. * Updated `OmegaConfigLoader` to handle paths containing dots outside of `conf_source`. +* Made `settings.py` optional. ## Documentation changes * Added documentation to clarify execution order of hooks. diff --git a/kedro/framework/project/__init__.py b/kedro/framework/project/__init__.py index d587c4ddf8..56f8a6e014 100644 --- a/kedro/framework/project/__init__.py +++ b/kedro/framework/project/__init__.py @@ -259,7 +259,7 @@ def configure_logging(logging_config: dict[str, Any]) -> None: def validate_settings(): - """Eagerly validate that the settings module is importable. This is desirable to + """Eagerly validate that the settings module is importable if it exists. This is desirable to surface any syntax or import errors early. In particular, without eagerly importing the settings module, dynaconf would silence any import error (e.g. missing dependency, missing/mislabelled pipeline), and users would instead get a cryptic @@ -272,8 +272,12 @@ def validate_settings(): "'bootstrap_project'. This should happen automatically if you are using " "Kedro command line interface." ) - - importlib.import_module(f"{PACKAGE_NAME}.settings") + # Check if file exists, if it does, validate it. + if importlib.util.find_spec(f"{PACKAGE_NAME}.settings") is not None: # type: ignore + importlib.import_module(f"{PACKAGE_NAME}.settings") + else: + logger = logging.getLogger(__name__) + logger.warning("No 'settings.py' found, defaults will be used.") def _create_pipeline(pipeline_module: types.ModuleType) -> Pipeline | None: diff --git a/tests/framework/project/test_settings.py b/tests/framework/project/test_settings.py index 65774e0e37..27372854db 100644 --- a/tests/framework/project/test_settings.py +++ b/tests/framework/project/test_settings.py @@ -1,3 +1,4 @@ +import importlib import sys import textwrap @@ -69,6 +70,18 @@ def mock_package_name_with_settings_file(tmpdir): settings.set(key, value) +@pytest.fixture +def mock_package_name_without_settings_file(tmpdir): + """This mocks a project that doesn't have a settings.py file. + When configured, the project should have sensible default settings.""" + package_name = "test_package_without_settings" + project_path, _, _ = str(tmpdir.mkdir(package_name)).rpartition(package_name) + + sys.path.insert(0, project_path) + yield package_name + sys.path.pop(0) + + def test_settings_without_configure_project_shows_default_values(): assert len(settings.HOOKS) == 0 assert settings.DISABLE_HOOKS_FOR_PLUGINS.to_list() == [] @@ -96,6 +109,19 @@ def test_settings_after_configuring_project_shows_updated_values( assert settings.DATA_CATALOG_CLASS == MyDataCatalog +def test_validate_settings_without_settings_file( + mock_package_name_without_settings_file, +): + assert ( + importlib.util.find_spec(f"{mock_package_name_without_settings_file}.settings") + is None + ) + configure_project(mock_package_name_without_settings_file) + validate_settings() + # When a kedro project doesn't have a settings file, the settings should match be the default. + test_settings_without_configure_project_shows_default_values() + + def test_validate_settings_with_empty_package_name(): with pytest.raises(ValueError): configure_project(None) # Simulate outside of project mode