mirror of
https://github.com/getpelican/pelican.git
synced 2026-10-27 15:37:02 +01:00
Raise a clear error for a missing/unsupported settings file
load_source() passed the path straight to importlib.util.spec_from_file_location() and then accessed spec.loader without checking spec first. When the path has no extension or an extension importlib doesn't recognize as a source file (e.g. the user passed 'publishconf' instead of 'publishconf.py'), spec_from_file_location returns None, and the subsequent module_from_spec(None) raises a cryptic "AttributeError: 'NoneType' object has no attribute 'loader'" that names neither the missing file nor its path. Check the file exists first and raise FileNotFoundError naming the path; if it exists but importlib still can't build a loader for it, raise ImportError explaining the file type isn't supported. Fixes #3614.
This commit is contained in:
parent
3c69dc68d2
commit
857833b94e
2 changed files with 36 additions and 0 deletions
|
|
@ -16,7 +16,15 @@ from pelican.paginator import PaginationRule
|
|||
|
||||
|
||||
def load_source(name: str, path: str) -> ModuleType:
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"Settings file not found: {path}")
|
||||
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(
|
||||
f"Unable to load settings file (unsupported file type): {path}"
|
||||
)
|
||||
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from pelican.settings import (
|
|||
_printf_s_to_format_field,
|
||||
configure_settings,
|
||||
handle_deprecated_settings,
|
||||
load_source,
|
||||
read_settings,
|
||||
)
|
||||
from pelican.tests.support import LogCountHandler, unittest
|
||||
|
|
@ -73,6 +74,33 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
settings["SITENAME"] = "Not a Pelican Blog"
|
||||
self.assertNotEqual(settings["SITENAME"], DEFAULT_CONFIG["SITENAME"])
|
||||
|
||||
def test_load_source_missing_file_reports_path(self):
|
||||
# A missing settings file should raise a clear error naming the
|
||||
# path, not the cryptic "'NoneType' object has no attribute
|
||||
# 'loader'" that importlib raises when spec_from_file_location
|
||||
# can't determine a loader (e.g. no/unknown file extension).
|
||||
missing_path = join(self.PATH, "this_settings_file_does_not_exist")
|
||||
with self.assertRaises(FileNotFoundError) as ctx:
|
||||
load_source("test_missing", missing_path)
|
||||
self.assertIn(missing_path, str(ctx.exception))
|
||||
|
||||
def test_load_source_missing_file_with_extension_reports_path(self):
|
||||
missing_path = join(self.PATH, "this_settings_file_does_not_exist.py")
|
||||
with self.assertRaises(FileNotFoundError) as ctx:
|
||||
load_source("test_missing_py", missing_path)
|
||||
self.assertIn(missing_path, str(ctx.exception))
|
||||
|
||||
def test_load_source_unsupported_extension_reports_path(self):
|
||||
unsupported_path = join(self.PATH, "default_conf.py.bak")
|
||||
with open(unsupported_path, "w") as f:
|
||||
f.write("SITENAME = 'unused'\n")
|
||||
try:
|
||||
with self.assertRaises(ImportError) as ctx:
|
||||
load_source("test_unsupported", unsupported_path)
|
||||
self.assertIn(unsupported_path, str(ctx.exception))
|
||||
finally:
|
||||
os.remove(unsupported_path)
|
||||
|
||||
def test_static_path_settings_safety(self):
|
||||
# Disallow static paths from being strings
|
||||
settings = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue