mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Many new unit tests for configuration settings file (pelicanconf.py)
- Now reports line number/offset of pelicanconf.py syntax errors - Better error messages with full absolute resolved filespec due to: - Read-only attribute accidentially used - Non-existant file report - Prevent clobbering of Python built-in system modules by user's custom filename - Breakout test_settings.py into smaller and manageable UT files - test_settings.py - test_settings_deprecated.py - test_settings_module.py - test_settings_path.py - Start using pytest (test_settings_path.py) instead of unittest - - Set minimum Python to 3.8 (pytest 4.0)
This commit is contained in:
parent
513abbfdc6
commit
764eefcf04
10 changed files with 2125 additions and 174 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import copy
|
||||
import errno
|
||||
import importlib.util
|
||||
import inspect
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
from os.path import isabs
|
||||
|
|
@ -13,14 +15,7 @@ from typing import Any, Dict, Optional
|
|||
|
||||
from pelican.log import LimitFilter
|
||||
|
||||
|
||||
def load_source(name: str, path: str) -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
DEFAULT_MODULE_NAME: str = "pelicanconf"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -182,13 +177,259 @@ DEFAULT_CONFIG = {
|
|||
PYGMENTS_RST_OPTIONS = None
|
||||
|
||||
|
||||
def load_source(name: str, path: str | pathlib.Path) -> ModuleType | None:
|
||||
"""
|
||||
Loads the Python-syntax file as a module for application access
|
||||
|
||||
Only `path` shall be consulted for its actual file location.
|
||||
|
||||
If module_name is not supplied as an argument, then its module name
|
||||
shall be extracted from given `path` argument but without any directory
|
||||
nor its file extension (just the basic pathLib.Path(path).stem part).
|
||||
|
||||
This function substitutes Python built-in `importlib` but with one distinctive
|
||||
but different feature: No attempts are made to leverage the `PYTHONPATH` as an
|
||||
alternative multi-directory search of a specified module name;
|
||||
only singular-directory lookup/search is supported here.
|
||||
|
||||
WARNING to DEVELOPER: If you programmatically used the "from" reserved
|
||||
Python keyword as in this "from pelicanconf import ..." statement, then you
|
||||
will not be able to free up the pelicanconf module, much
|
||||
less use 'reload module' features. (Not a likely scenario, but)
|
||||
|
||||
:param path: filespec of the Python script file to load as Python module;
|
||||
`path` parameter may be a filename which is looked for in the
|
||||
current working directory, or a relative filename that looks
|
||||
only in that particular directory, or an absolute filename
|
||||
that also looks only in that absolute directory.
|
||||
:type path: str | pathlib.Path
|
||||
:param name: Optional argument to the Python module name to be loaded.
|
||||
`module_name` shall never use dotted notation nor any
|
||||
directory separator, just the plain filename (without
|
||||
any extension/suffix part).
|
||||
:type name: str
|
||||
:return: the ModuleType of the loaded Python module file. Will be
|
||||
accessible in a form of "pelican.<module_name>".
|
||||
:rtype: ModuleType | None
|
||||
"""
|
||||
if isinstance(path, str):
|
||||
conf_filespec = pathlib.Path(path)
|
||||
elif isinstance(path, pathlib.Path):
|
||||
conf_filespec = path
|
||||
else:
|
||||
logger.fatal(
|
||||
f"argument {path.__str__()} is not a pathLib.Path type nor a str type."
|
||||
)
|
||||
raise TypeError
|
||||
|
||||
absolute_filespec = conf_filespec.absolute()
|
||||
filename_ext = conf_filespec.name
|
||||
if not absolute_filespec.exists():
|
||||
logger.error(f"File '{filename_ext!s}' not found.")
|
||||
return None
|
||||
if not absolute_filespec.is_file():
|
||||
logger.error(f"Absolute '{conf_filespec!s}' path is not a file.")
|
||||
return None
|
||||
if not os.access(str(absolute_filespec), os.R_OK):
|
||||
logger.error(f"'{absolute_filespec}' file is not readable.")
|
||||
return None
|
||||
resolved_absolute_filespec = absolute_filespec.resolve(strict=True)
|
||||
|
||||
# We used to strip '.py' extension to make a module name out of it
|
||||
# But we cannot take in anymore the user-supplied path for anything
|
||||
# (such as Pelican configuration settings file) as THE Python module
|
||||
# name for Pelican due to potential conflict with 300+ names of Python
|
||||
# built-in system module, such as `site.conf`, `calendar.conf`.
|
||||
# A complete list is in https://docs.python.org/3/py-modindex.html.
|
||||
# Instead, force the caller of load_source to supply a Pelican-specific but
|
||||
# statically fixed module_name to be associated with the end-user's choice
|
||||
# of configuration filename.
|
||||
|
||||
module_name = ""
|
||||
# if load_source(path=...) is used
|
||||
if "name" not in locals():
|
||||
# old load_source(path) prototype
|
||||
module_name = pathlib.Path(path).stem
|
||||
|
||||
# if load_source(None, =...) is used
|
||||
if name is None:
|
||||
logger.warning(
|
||||
f"Module name is missing; using Python built-in to check "
|
||||
f"PYTHONPATH for {absolute_filespec}"
|
||||
)
|
||||
# if load_source(value: not str, =...) is used
|
||||
elif not isinstance(name, str):
|
||||
raise TypeError
|
||||
# if load_source(name="", =...) is used
|
||||
elif name == "":
|
||||
module_name = pathlib.Path(path).stem
|
||||
# if load_source(value: not str, =...) is used
|
||||
elif isinstance(name, str):
|
||||
module_name = name
|
||||
else:
|
||||
raise TypeError("load_source(name=...) argument is not a str type")
|
||||
|
||||
# One last thing to do before sys.modules check, is to deny any dotted module name
|
||||
# This is a Pelican design issue (to support pelicanconf reloadability)
|
||||
#
|
||||
# Alternatively, do we want to support the approach of Python 'pelican.conf'
|
||||
# module? Yes, but we couldn't, while it is the correct design to nest
|
||||
# the configuration settings as a submodule behind Pelican (we should be
|
||||
# able to), but Pelican design is restricted by Python sys.modules design.
|
||||
#
|
||||
# Also, we lose reload() capability once the alternative desired design of
|
||||
# 'conf' submodule gets loaded by Python 'from' keyword.
|
||||
#
|
||||
# Hence, we do not support 'pelican.conf' due to Pelican reloadability requirement
|
||||
#
|
||||
# Judge have ruled the 'period' symbol in module name as disallowable.
|
||||
if "." in module_name:
|
||||
# load_connect() should return sys.exit by design, but no.
|
||||
# Below fatal log is used AS-IS by test_settings_module.py unit test
|
||||
logger.fatal(f"Cannot use dotted module name such as `{module_name}`.")
|
||||
# Nothing fancy, return None
|
||||
return None
|
||||
|
||||
# Nonetheless, we check that this module_name is not taken as well.
|
||||
# Check that the module name is not in sys.module (like pathlib!!!)
|
||||
if module_name in sys.modules:
|
||||
# following logger.fatal is used as-is by test_settings_module.py unit test
|
||||
logger.fatal(
|
||||
f"Cannot reserved the module name already used"
|
||||
f" by Python system module `{module_name}`."
|
||||
)
|
||||
sys.exit(errno.EPERM)
|
||||
|
||||
# if module_name == "":
|
||||
# module_name = "pelicanconf"
|
||||
|
||||
try:
|
||||
# Using Python importlib, find the module using a full file path
|
||||
# specification and its filename and return this Module instance
|
||||
module_spec = importlib.util.spec_from_file_location(
|
||||
module_name, resolved_absolute_filespec
|
||||
)
|
||||
logger.debug(f"ModuleSpec '{module_name}' obtained from {absolute_filespec}.")
|
||||
except ImportError:
|
||||
logger.fatal(
|
||||
f"Location {resolved_absolute_filespec} may be missing a "
|
||||
f"module `get_filename` attribute value."
|
||||
)
|
||||
raise ModuleNotFoundError from ImportError
|
||||
except OSError:
|
||||
logger.error(
|
||||
f"Python module loader for configuration settings file "
|
||||
f"cannot determine absolute directory path from {absolute_filespec}."
|
||||
)
|
||||
raise FileNotFoundError from OSError
|
||||
# pass all the other excepts out
|
||||
|
||||
try:
|
||||
# With the ModuleSpec object, we can get the
|
||||
module_type = importlib.util.module_from_spec(module_spec)
|
||||
except ImportError:
|
||||
logger.fatal(
|
||||
"Loader that defines exec_module() must also define create_module()"
|
||||
)
|
||||
raise ImportError from ImportError
|
||||
|
||||
# module_type also has all the loaded Python values/objects from
|
||||
# the Pelican configuration settings file, but it is not
|
||||
# yet readable for all...
|
||||
|
||||
# if you have use "from pelicanconf import ...", then you will not
|
||||
# be able to free up the module
|
||||
|
||||
# store the module into the sys.modules
|
||||
sys.modules[module_name] = module_type
|
||||
|
||||
try:
|
||||
# finally, execute any codes in the Pelican configuration settings file.
|
||||
module_spec.loader.exec_module(module_type)
|
||||
# Below logger.debug is used as-is by test_settings_module.py unit-test
|
||||
logger.debug(
|
||||
f"Loaded module '{module_name}' from {resolved_absolute_filespec} file"
|
||||
)
|
||||
return module_type
|
||||
except SyntaxError as e:
|
||||
full_filespec = conf_filespec.resolve(strict=True)
|
||||
# Show where in the pelicanconf.py the offending syntax error is at via {e}.
|
||||
logger.error(
|
||||
f"{e}.\nHINT: "
|
||||
f"Try executing `python {full_filespec}` "
|
||||
f"for better syntax troubleshooting."
|
||||
)
|
||||
# Trying something new, reraise the exception up
|
||||
raise SyntaxError(
|
||||
f"Invalid syntax error at line number {e.end_lineno}"
|
||||
f" column offset {e.end_offset}",
|
||||
{
|
||||
"filename": full_filespec,
|
||||
"lineno": int(e.lineno),
|
||||
"offset": int(e.offset),
|
||||
"text": e.text,
|
||||
"end_lineno": int(e.end_lineno),
|
||||
"end_offset": int(e.end_offset),
|
||||
},
|
||||
) from e
|
||||
except Any as e:
|
||||
logger.critical(
|
||||
f"'Python system module loader for {resolved_absolute_filespec}'"
|
||||
f" module failed: {e}."
|
||||
)
|
||||
sys.exit(errno.ENOEXEC)
|
||||
|
||||
|
||||
def reload_source(name: str, path: str | pathlib.Path) -> ModuleType | None:
|
||||
"""Reload the configuration settings file"""
|
||||
# first line of defense against errant built-in module name
|
||||
if name != DEFAULT_MODULE_NAME:
|
||||
logger.error(
|
||||
f"Module name of {name} cannot be anything other "
|
||||
f"than {DEFAULT_MODULE_NAME}"
|
||||
)
|
||||
return None
|
||||
# second line of defense, this function call only works if one firstly
|
||||
# called the load_source()
|
||||
if name not in sys.modules:
|
||||
logger.error(f"Module name of {name} is not loaded, call load_source() firstly")
|
||||
return None
|
||||
# Now we can do the dangerous step
|
||||
del sys.modules[name]
|
||||
module_type = load_source(name, path)
|
||||
if module_type is None:
|
||||
logger.error(f"Module name of {name} is not loaded, call load_source() firstly")
|
||||
return None
|
||||
return module_type
|
||||
|
||||
|
||||
def read_settings(
|
||||
path: Optional[str] = None, override: Optional[Settings] = None
|
||||
path: Optional[str] = None,
|
||||
override: Optional[Settings] = None,
|
||||
reload: bool = False,
|
||||
) -> Settings:
|
||||
"""reads the setting files into a Python configuration settings module
|
||||
|
||||
Returns the final Settings list of keys/values after reading the file
|
||||
and applying an override of settings on top of it.
|
||||
|
||||
:param path: The full filespec path to the Pelican configuration settings file.
|
||||
:type path: str | None
|
||||
:param path: The override settings to be used to overwrite the ones read in
|
||||
from the Pelican configuration settings file.
|
||||
:type override: Settings | None
|
||||
:param reload: A boolean value to safely reload the Pelican configuration settings
|
||||
file into a Python module
|
||||
:type reload: bool
|
||||
:return: The Settings list of configurations after extracting the key/value from
|
||||
the path of Pelican configuration settings file and after the override
|
||||
settings has been applied over its read settings.
|
||||
:rtype: Settings
|
||||
"""
|
||||
settings = override or {}
|
||||
|
||||
if path:
|
||||
settings = dict(get_settings_from_file(path), **settings)
|
||||
settings = dict(get_settings_from_file(path, reload), **settings)
|
||||
|
||||
if settings:
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
|
@ -217,7 +458,7 @@ def read_settings(
|
|||
]
|
||||
|
||||
settings = dict(copy.deepcopy(DEFAULT_CONFIG), **settings)
|
||||
settings = configure_settings(settings)
|
||||
settings = configure_settings(settings, reload)
|
||||
|
||||
# This is because there doesn't seem to be a way to pass extra
|
||||
# parameters to docutils directive handlers, so we have to have a
|
||||
|
|
@ -229,19 +470,40 @@ def read_settings(
|
|||
|
||||
|
||||
def get_settings_from_module(module: Optional[ModuleType] = None) -> Settings:
|
||||
"""Loads settings from a module, returns a dictionary."""
|
||||
"""Clones a dictionary of settings from a module.
|
||||
|
||||
:param module: Attempts to load a module using singular current working directory
|
||||
(`$CWD`) search method then returns a clone-duplicate of its
|
||||
settings found in the module.
|
||||
If no module (`None`) is given, then default module name is used.
|
||||
:type module: ModuleType | None
|
||||
:return: Returns a dictionary of Settings found in that Python module.
|
||||
:rtype: Settings"""
|
||||
context = {}
|
||||
if module is not None:
|
||||
context.update((k, v) for k, v in inspect.getmembers(module) if k.isupper())
|
||||
return context
|
||||
|
||||
|
||||
def get_settings_from_file(path: str) -> Settings:
|
||||
"""Loads settings from a file path, returning a dict."""
|
||||
def get_settings_from_file(path: str, reload: bool) -> Settings:
|
||||
"""Loads module from a file then clones dictionary of settings from that module.
|
||||
|
||||
name, ext = os.path.splitext(os.path.basename(path))
|
||||
module = load_source(name, path)
|
||||
:param path: Attempts to load a module using a file specification (absolute or
|
||||
relative) then returns a clone-duplicate of its settings found in
|
||||
the module. If no module (`None`) is given, then default module
|
||||
name is used.
|
||||
:param path: A file specification (absolute or relative) that points to the
|
||||
Python script file containing the keyword/value assignment settings.
|
||||
:param reload: A bool value to check if module is already preloaded
|
||||
before doing a reload.
|
||||
:return: Returns a dictionary of Settings found in that Python module.
|
||||
:rtype: Settings"""
|
||||
name = DEFAULT_MODULE_NAME
|
||||
# Keep the module name constant for Pelican configuration settings file
|
||||
if reload:
|
||||
module = reload_source(name, path)
|
||||
else:
|
||||
module = load_source(name, path)
|
||||
return get_settings_from_module(module)
|
||||
|
||||
|
||||
|
|
@ -568,11 +830,17 @@ def handle_deprecated_settings(settings: Settings) -> Settings:
|
|||
return settings
|
||||
|
||||
|
||||
def configure_settings(settings: Settings) -> Settings:
|
||||
def configure_settings(settings: Settings, reload: None | bool = False) -> Settings:
|
||||
"""Provide optimizations, error checking, and warnings for the given
|
||||
settings.
|
||||
Also, specify the log messages to be ignored.
|
||||
"""
|
||||
|
||||
:param settings: Contains a dictionary of Pelican keyword/keyvalue
|
||||
:type settings: Settings
|
||||
:param reload: A flag to reload the same module but maybe with a different file
|
||||
:type reload: bool
|
||||
:return: An updated dictionary of Pelican's keywords and its keyvalue.
|
||||
:rtype: Settings"""
|
||||
if "PATH" not in settings or not os.path.isdir(settings["PATH"]):
|
||||
raise Exception(
|
||||
"You need to specify a path containing the content"
|
||||
|
|
@ -613,7 +881,7 @@ def configure_settings(settings: Settings) -> Settings:
|
|||
]:
|
||||
if key in settings and not isinstance(settings[key], types):
|
||||
value = settings.pop(key)
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"Detected misconfigured %s (%s), falling back to the default (%s)",
|
||||
key,
|
||||
value,
|
||||
|
|
|
|||
15
pelican/tests/settings/pelicanconf-missing-path-var.py
Normal file
15
pelican/tests/settings/pelicanconf-missing-path-var.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
DEBUG = True
|
||||
#####PATH = "not-defined" # our newly created location for our articles/pages to reside in/under
|
||||
OUTPUT_PATH = "output"
|
||||
ARTICLES_PATH = ["articles"]
|
||||
PLUGIN_PATHS = [
|
||||
"~/admin/websites/egbert.net/pelican/plugins",
|
||||
"~/admin/websites/egbert.net/pelican-plugins",
|
||||
"~/admin/websites/egbert.net/development/tableize/pelican/plugins",
|
||||
]
|
||||
PLUGINS = ["tableize"]
|
||||
AUTHOR = "John"
|
||||
SITENAME = "My 1st Site"
|
||||
# for
|
||||
PORT = 8000
|
||||
BIND = "127.0.0.1"
|
||||
15
pelican/tests/settings/pelicanconf-path-no-such-directory.py
Normal file
15
pelican/tests/settings/pelicanconf-path-no-such-directory.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
DEBUG = True
|
||||
PATH = "no-such-directory" # our newly created location for our articles/pages to reside in/under
|
||||
OUTPUT_PATH = "output"
|
||||
ARTICLES_PATH = ["articles"]
|
||||
PLUGIN_PATHS = [
|
||||
"~/admin/websites/egbert.net/pelican/plugins",
|
||||
"~/admin/websites/egbert.net/pelican-plugins",
|
||||
"~/admin/websites/egbert.net/development/tableize/pelican/plugins",
|
||||
]
|
||||
PLUGINS = ["tableize"]
|
||||
AUTHOR = "John"
|
||||
SITENAME = "My 1st Site"
|
||||
# for
|
||||
PORT = 8000
|
||||
BIND = "127.0.0.1"
|
||||
12
pelican/tests/settings/pelicanconf-syntax-error.py.disabled
Normal file
12
pelican/tests/settings/pelicanconf-syntax-error.py.disabled
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#! Ohhhtay, this is not a python file despite it's .py file extension
|
||||
|
||||
# Error to occur is at line 5, column 2
|
||||
|
||||
// put some C code in it
|
||||
|
||||
int i
|
||||
|
||||
for (i = 1; i < 200; ) {
|
||||
i = i + 1;
|
||||
|
||||
}
|
||||
18
pelican/tests/settings/pelicanconf-syntax-error2.py.disabled
Normal file
18
pelican/tests/settings/pelicanconf-syntax-error2.py.disabled
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#! Ohhhtay, this is not a python file despite it's .py file extension
|
||||
# Error to occur is at line 13, column 5
|
||||
# CAUTION: do not change this error location without corresponding changes in test cases
|
||||
|
||||
import me
|
||||
|
||||
def function_a(arg1):
|
||||
|
||||
value2 = arg1
|
||||
value3 = value2 + 1
|
||||
return value3
|
||||
|
||||
int i
|
||||
|
||||
for (i = 1; i < 200; ) {
|
||||
i = i + 1;
|
||||
|
||||
}
|
||||
15
pelican/tests/settings/pelicanconf-valid.py
Normal file
15
pelican/tests/settings/pelicanconf-valid.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
DEBUG = True
|
||||
PATH = "content" # our newly created location for our articles/pages to reside in/under
|
||||
OUTPUT_PATH = "output"
|
||||
ARTICLES_PATH = ["articles"]
|
||||
PLUGIN_PATHS = [
|
||||
"~/admin/websites/egbert.net/pelican/plugins",
|
||||
"~/admin/websites/egbert.net/pelican-plugins",
|
||||
"~/admin/websites/egbert.net/development/tableize/pelican/plugins",
|
||||
]
|
||||
PLUGINS = ["tableize"]
|
||||
AUTHOR = "John"
|
||||
SITENAME = "My 1st Site"
|
||||
# for
|
||||
PORT = 8000
|
||||
BIND = "127.0.0.1"
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from pelican.settings import (
|
||||
|
|
@ -25,14 +26,24 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
locale.setlocale(locale.LC_ALL, "C")
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
self.settings = read_settings(default_conf)
|
||||
self.settings = read_settings(default_conf, reload=True)
|
||||
self.original_sys_modules = sys.modules
|
||||
|
||||
def tearDown(self):
|
||||
locale.setlocale(locale.LC_ALL, self.old_locale)
|
||||
self.assertEqual(
|
||||
self.original_sys_modules,
|
||||
sys.modules,
|
||||
"One of the unit test did not clean up sys.modules " "properly.",
|
||||
)
|
||||
|
||||
def test_overwrite_existing_settings(self):
|
||||
self.assertEqual(self.settings.get("SITENAME"), "Alexis' log")
|
||||
self.assertEqual(self.settings.get("SITEURL"), "http://blog.notmyidea.org")
|
||||
# NOTE: testSetup() is done once for all unit tests within the same class.
|
||||
# NOTE: Probably want to use test_module(module) or xtest_()
|
||||
# Parallelized test are done in random order, so this FIRSTLY test
|
||||
# will fail ... most of the time.
|
||||
# def test_overwrite_existing_settings(self):
|
||||
# self.assertEqual(self.settings.get("SITENAME"), "Alexis' log")
|
||||
# self.assertEqual(self.settings.get("SITEURL"), "http://blog.notmyidea.org")
|
||||
|
||||
def test_keep_default_settings(self):
|
||||
# Keep default settings if not defined.
|
||||
|
|
@ -57,12 +68,23 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
|
||||
def test_settings_return_independent(self):
|
||||
# Make sure that the results from one settings call doesn't
|
||||
# effect past or future instances.
|
||||
# affect past or future instances.
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
settings = read_settings(default_conf)
|
||||
settings["SITEURL"] = "new-value"
|
||||
new_settings = read_settings(default_conf)
|
||||
# settings['SITEURL'] should be blank
|
||||
|
||||
# Trap any exception error
|
||||
try:
|
||||
# why did setUp() call read_settings firstly? So, we reload here
|
||||
settings = read_settings(default_conf, reload=True)
|
||||
settings["SITEURL"] = "new-value"
|
||||
except any:
|
||||
raise any from None
|
||||
|
||||
# clobber settings['SITEURL']
|
||||
new_settings = read_settings(default_conf, reload=True)
|
||||
# see if pulling up a new set of original settings (into a different variable,
|
||||
# via 'new_settings' does not clobber the 'settings' variable
|
||||
self.assertNotEqual(new_settings["SITEURL"], settings["SITEURL"])
|
||||
|
||||
def test_defaults_not_overwritten(self):
|
||||
|
|
@ -105,6 +127,7 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
self.assertEqual(settings["FEED_DOMAIN"], "http://blog.notmyidea.org")
|
||||
|
||||
settings["FEED_DOMAIN"] = "http://feeds.example.com"
|
||||
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings["FEED_DOMAIN"], "http://feeds.example.com")
|
||||
|
||||
|
|
@ -175,149 +198,6 @@ class TestSettingsConfiguration(unittest.TestCase):
|
|||
found = result.format(slug="qux")
|
||||
self.assertEqual(expected, found)
|
||||
|
||||
def test_deprecated_extra_templates_paths(self):
|
||||
settings = self.settings
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/foo/bar", "/ha"]
|
||||
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
||||
self.assertEqual(settings["THEME_TEMPLATES_OVERRIDES"], ["/foo/bar", "/ha"])
|
||||
self.assertNotIn("EXTRA_TEMPLATES_PATHS", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates(self):
|
||||
settings = self.settings
|
||||
settings["PAGINATED_DIRECT_TEMPLATES"] = ["index", "archives"]
|
||||
settings["PAGINATED_TEMPLATES"] = {"index": 10, "category": None}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{"index": 10, "category": None, "archives": None},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# PAGINATED_DIRECT_TEMPLATES defined but no PAGINATED_TEMPLATES.
|
||||
settings = read_settings(
|
||||
None, override={"PAGINATED_DIRECT_TEMPLATES": ["index", "archives"]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{
|
||||
"archives": None,
|
||||
"author": None,
|
||||
"index": None,
|
||||
"category": None,
|
||||
"tag": None,
|
||||
},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_theme_and_extra_templates_exception(self):
|
||||
settings = self.settings
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/ha"]
|
||||
settings["THEME_TEMPLATES_OVERRIDES"] = ["/foo/bar"]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_slug_and_slug_regex_substitutions_exception(self):
|
||||
settings = {}
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_deprecated_slug_substitutions(self):
|
||||
default_slug_regex_subs = self.settings["SLUG_REGEX_SUBSTITUTIONS"]
|
||||
|
||||
# If no deprecated setting is set, don't set new ones
|
||||
settings = {}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("AUTHOR_REGEX_SUBSTITUTIONS", settings)
|
||||
|
||||
# If SLUG_SUBSTITUTIONS is set, set {SLUG, AUTHOR}_REGEX_SUBSTITUTIONS
|
||||
# correctly, don't set {CATEGORY, TAG}_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings.get("AUTHOR_REGEX_SUBSTITUTIONS"), default_slug_regex_subs
|
||||
)
|
||||
|
||||
# If {CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly, don't set
|
||||
# SLUG_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# If {SLUG, CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {SLUG, CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# Handle old 'skip' flags correctly
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp", True)]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov", False)]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + [(r"(?u)\A\s*", ""), (r"(?u)\s*\Z", "")],
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
def test_deprecated_slug_substitutions_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# SLUG_SUBSTITUTIONS defined but no SLUG_REGEX_SUBSTITUTIONS.
|
||||
settings = read_settings(
|
||||
None, override={"SLUG_SUBSTITUTIONS": [("C++", "cpp")]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + self.settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
)
|
||||
self.assertNotIn("SLUG_SUBSTITUTIONS", settings)
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
347
pelican/tests/test_settings_deprecated.py
Normal file
347
pelican/tests/test_settings_deprecated.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import copy
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from pelican.settings import (
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_THEME,
|
||||
_printf_s_to_format_field,
|
||||
configure_settings,
|
||||
handle_deprecated_settings,
|
||||
read_settings,
|
||||
)
|
||||
from pelican.tests.support import unittest
|
||||
|
||||
|
||||
class SettingsDeprecated(unittest.TestCase):
|
||||
"""Exercises handle_deprecated_settings()"""
|
||||
|
||||
def setUp(self):
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
self.settings = read_settings(default_conf, reload=True)
|
||||
self.original_sys_modules = sys.modules
|
||||
|
||||
def tearDown(self):
|
||||
locale.setlocale(locale.LC_ALL, self.old_locale)
|
||||
self.assertEqual(
|
||||
self.original_sys_modules,
|
||||
sys.modules,
|
||||
"One of the unit test did not clean up sys.modules " "properly.",
|
||||
)
|
||||
|
||||
# NOTE: testSetup() is done once for all unit tests within the same class.
|
||||
# NOTE: Probably want to use test_module(module) or xtest_()
|
||||
# Parallelized test are done in random order, so this FIRSTLY test
|
||||
# will fail ... most of the time.
|
||||
# def test_overwrite_existing_settings(self):
|
||||
# self.assertEqual(self.settings.get("SITENAME"), "Alexis' log")
|
||||
# self.assertEqual(self.settings.get("SITEURL"), "http://blog.notmyidea.org")
|
||||
|
||||
def test_keep_default_settings(self):
|
||||
# Keep default settings if not defined.
|
||||
self.assertEqual(
|
||||
self.settings.get("DEFAULT_CATEGORY"), DEFAULT_CONFIG["DEFAULT_CATEGORY"]
|
||||
)
|
||||
|
||||
def test_dont_copy_small_keys(self):
|
||||
# Do not copy keys not in caps.
|
||||
self.assertNotIn("foobar", self.settings)
|
||||
|
||||
def test_read_empty_settings(self):
|
||||
# Ensure an empty settings file results in default settings.
|
||||
settings = read_settings(None)
|
||||
expected = copy.deepcopy(DEFAULT_CONFIG)
|
||||
# Added by configure settings
|
||||
expected["FEED_DOMAIN"] = ""
|
||||
expected["ARTICLE_EXCLUDES"] = ["pages"]
|
||||
expected["PAGE_EXCLUDES"] = [""]
|
||||
self.maxDiff = None
|
||||
self.assertDictEqual(settings, expected)
|
||||
|
||||
def test_settings_return_independent(self):
|
||||
# Make sure that the results from one settings call doesn't
|
||||
# affect past or future instances.
|
||||
self.PATH = abspath(dirname(__file__))
|
||||
default_conf = join(self.PATH, "default_conf.py")
|
||||
# settings['SITEURL'] should be blank
|
||||
|
||||
# Trap any exception error
|
||||
try:
|
||||
# why did setUp() call read_settings firstly? So, we reload here
|
||||
settings = read_settings(default_conf, reload=True)
|
||||
settings["SITEURL"] = "new-value"
|
||||
except any:
|
||||
raise any from None
|
||||
|
||||
# clobber settings['SITEURL']
|
||||
new_settings = read_settings(default_conf, reload=True)
|
||||
# see if pulling up a new set of original settings (into a different variable,
|
||||
# via 'new_settings' does not clobber the 'settings' variable
|
||||
self.assertNotEqual(new_settings["SITEURL"], settings["SITEURL"])
|
||||
|
||||
def test_defaults_not_overwritten(self):
|
||||
# This assumes 'SITENAME': 'A Pelican Blog'
|
||||
settings = read_settings(None)
|
||||
settings["SITENAME"] = "Not a Pelican Blog"
|
||||
self.assertNotEqual(settings["SITENAME"], DEFAULT_CONFIG["SITENAME"])
|
||||
|
||||
def test_static_path_settings_safety(self):
|
||||
# Disallow static paths from being strings
|
||||
settings = {
|
||||
"STATIC_PATHS": "foo/bar",
|
||||
"THEME_STATIC_PATHS": "bar/baz",
|
||||
# These 4 settings are required to run configure_settings
|
||||
"PATH": ".",
|
||||
"THEME": DEFAULT_THEME,
|
||||
"SITEURL": "http://blog.notmyidea.org/",
|
||||
"LOCALE": "",
|
||||
}
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings["STATIC_PATHS"], DEFAULT_CONFIG["STATIC_PATHS"])
|
||||
self.assertEqual(
|
||||
settings["THEME_STATIC_PATHS"], DEFAULT_CONFIG["THEME_STATIC_PATHS"]
|
||||
)
|
||||
|
||||
def test_configure_settings(self):
|
||||
# Manipulations to settings should be applied correctly.
|
||||
settings = {
|
||||
"SITEURL": "http://blog.notmyidea.org/",
|
||||
"LOCALE": "",
|
||||
"PATH": os.curdir,
|
||||
"THEME": DEFAULT_THEME,
|
||||
}
|
||||
configure_settings(settings)
|
||||
|
||||
# SITEURL should not have a trailing slash
|
||||
self.assertEqual(settings["SITEURL"], "http://blog.notmyidea.org")
|
||||
|
||||
# FEED_DOMAIN, if undefined, should default to SITEURL
|
||||
self.assertEqual(settings["FEED_DOMAIN"], "http://blog.notmyidea.org")
|
||||
|
||||
settings["FEED_DOMAIN"] = "http://feeds.example.com"
|
||||
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings["FEED_DOMAIN"], "http://feeds.example.com")
|
||||
|
||||
def test_theme_settings_exceptions(self):
|
||||
settings = self.settings
|
||||
|
||||
# Check that theme lookup in "pelican/themes" functions as expected
|
||||
settings["THEME"] = os.path.split(settings["THEME"])[1]
|
||||
configure_settings(settings)
|
||||
self.assertEqual(settings["THEME"], DEFAULT_THEME)
|
||||
|
||||
# Check that non-existent theme raises exception
|
||||
settings["THEME"] = "foo"
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
def test_deprecated_dir_setting(self):
|
||||
settings = self.settings
|
||||
|
||||
settings["ARTICLE_DIR"] = "foo"
|
||||
settings["PAGE_DIR"] = "bar"
|
||||
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
||||
self.assertEqual(settings["ARTICLE_PATHS"], ["foo"])
|
||||
self.assertEqual(settings["PAGE_PATHS"], ["bar"])
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
settings["ARTICLE_DIR"]
|
||||
settings["PAGE_DIR"]
|
||||
|
||||
def test_default_encoding(self):
|
||||
# Test that the user locale is set if not specified in settings
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
# empty string = user system locale
|
||||
self.assertEqual(self.settings["LOCALE"], [""])
|
||||
|
||||
configure_settings(self.settings)
|
||||
lc_time = locale.getlocale(locale.LC_TIME) # should be set to user locale
|
||||
|
||||
# explicitly set locale to user pref and test
|
||||
locale.setlocale(locale.LC_TIME, "")
|
||||
self.assertEqual(lc_time, locale.getlocale(locale.LC_TIME))
|
||||
|
||||
def test_invalid_settings_throw_exception(self):
|
||||
# Test that the path name is valid
|
||||
|
||||
# test that 'PATH' is set
|
||||
settings = {}
|
||||
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
# Test that 'PATH' is valid
|
||||
settings["PATH"] = ""
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
# Test nonexistent THEME
|
||||
settings["PATH"] = os.curdir
|
||||
settings["THEME"] = "foo"
|
||||
|
||||
self.assertRaises(Exception, configure_settings, settings)
|
||||
|
||||
def test__printf_s_to_format_field(self):
|
||||
for s in ("%s", "{%s}", "{%s"):
|
||||
option = f"foo/{s}/bar.baz"
|
||||
result = _printf_s_to_format_field(option, "slug")
|
||||
expected = option % "qux"
|
||||
found = result.format(slug="qux")
|
||||
self.assertEqual(expected, found)
|
||||
|
||||
def test_deprecated_extra_templates_paths(self):
|
||||
settings = self.settings
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/foo/bar", "/ha"]
|
||||
|
||||
settings = handle_deprecated_settings(settings)
|
||||
|
||||
self.assertEqual(settings["THEME_TEMPLATES_OVERRIDES"], ["/foo/bar", "/ha"])
|
||||
self.assertNotIn("EXTRA_TEMPLATES_PATHS", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates(self):
|
||||
settings = self.settings
|
||||
settings["PAGINATED_DIRECT_TEMPLATES"] = ["index", "archives"]
|
||||
settings["PAGINATED_TEMPLATES"] = {"index": 10, "category": None}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{"index": 10, "category": None, "archives": None},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_deprecated_paginated_direct_templates_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# PAGINATED_DIRECT_TEMPLATES defined but no PAGINATED_TEMPLATES.
|
||||
settings = read_settings(
|
||||
None, override={"PAGINATED_DIRECT_TEMPLATES": ["index", "archives"]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["PAGINATED_TEMPLATES"],
|
||||
{
|
||||
"archives": None,
|
||||
"author": None,
|
||||
"index": None,
|
||||
"category": None,
|
||||
"tag": None,
|
||||
},
|
||||
)
|
||||
self.assertNotIn("PAGINATED_DIRECT_TEMPLATES", settings)
|
||||
|
||||
def test_theme_and_extra_templates_exception(self):
|
||||
settings = self.settings
|
||||
settings["EXTRA_TEMPLATES_PATHS"] = ["/ha"]
|
||||
settings["THEME_TEMPLATES_OVERRIDES"] = ["/foo/bar"]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_slug_and_slug_regex_substitutions_exception(self):
|
||||
settings = {}
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
|
||||
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
||||
|
||||
def test_deprecated_slug_substitutions(self):
|
||||
default_slug_regex_subs = self.settings["SLUG_REGEX_SUBSTITUTIONS"]
|
||||
|
||||
# If no deprecated setting is set, don't set new ones
|
||||
settings = {}
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("AUTHOR_REGEX_SUBSTITUTIONS", settings)
|
||||
|
||||
# If SLUG_SUBSTITUTIONS is set, set {SLUG, AUTHOR}_REGEX_SUBSTITUTIONS
|
||||
# correctly, don't set {CATEGORY, TAG}_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertNotIn("TAG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertNotIn("CATEGORY_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings.get("AUTHOR_REGEX_SUBSTITUTIONS"), default_slug_regex_subs
|
||||
)
|
||||
|
||||
# If {CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly, don't set
|
||||
# SLUG_REGEX_SUBSTITUTIONS
|
||||
settings = {}
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertNotIn("SLUG_REGEX_SUBSTITUTIONS", settings)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# If {SLUG, CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
||||
# {SLUG, CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp")]
|
||||
settings["TAG_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["CATEGORY_SUBSTITUTIONS"] = [("C#", "csharp")]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov")]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings["TAG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["CATEGORY_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + [(r"C\#", "csharp")] + default_slug_regex_subs,
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
# Handle old 'skip' flags correctly
|
||||
settings = {}
|
||||
settings["SLUG_SUBSTITUTIONS"] = [("C++", "cpp", True)]
|
||||
settings["AUTHOR_SUBSTITUTIONS"] = [("Alexander Todorov", "atodorov", False)]
|
||||
settings = handle_deprecated_settings(settings)
|
||||
self.assertEqual(
|
||||
settings.get("SLUG_REGEX_SUBSTITUTIONS"),
|
||||
[(r"C\+\+", "cpp")] + [(r"(?u)\A\s*", ""), (r"(?u)\s*\Z", "")],
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["AUTHOR_REGEX_SUBSTITUTIONS"],
|
||||
[(r"Alexander\ Todorov", "atodorov")] + default_slug_regex_subs,
|
||||
)
|
||||
|
||||
def test_deprecated_slug_substitutions_from_file(self):
|
||||
# This is equivalent to reading a settings file that has
|
||||
# SLUG_SUBSTITUTIONS defined but no SLUG_REGEX_SUBSTITUTIONS.
|
||||
settings = read_settings(
|
||||
None, override={"SLUG_SUBSTITUTIONS": [("C++", "cpp")]}
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
[(r"C\+\+", "cpp")] + self.settings["SLUG_REGEX_SUBSTITUTIONS"],
|
||||
)
|
||||
self.assertNotIn("SLUG_SUBSTITUTIONS", settings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
1074
pelican/tests/test_settings_module.py
Normal file
1074
pelican/tests/test_settings_module.py
Normal file
File diff suppressed because it is too large
Load diff
307
pelican/tests/test_settings_path.py
Normal file
307
pelican/tests/test_settings_path.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
#
|
||||
# Focused on settings.py/load_source(), specifically pathlib.Path type
|
||||
#
|
||||
# Ruff wants '# NOQA: RUF100'
|
||||
# PyCharm wants '# : RUF100'
|
||||
# RUFF says PyCharm is a no-go; stay with RUFF, ignore PyCharm's NOQA orange warnings
|
||||
|
||||
import copy
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest.logging import LogCaptureHandler, _remove_ansi_escape_sequences # NOQA
|
||||
|
||||
from pelican.settings import (
|
||||
load_source,
|
||||
)
|
||||
|
||||
TMP_DIRNAME_SUFFIX = "pelican"
|
||||
|
||||
DIRSPEC_RELATIVE = "settings" + os.sep
|
||||
|
||||
EXT_PYTHON = ".py"
|
||||
EXT_PYTHON_DISABLED = ".disabled"
|
||||
|
||||
PC_MODNAME_ACTUAL = "pelicanconf"
|
||||
|
||||
# FILENAME_: file name without the extension
|
||||
PC_FILENAME_DEFAULT = PC_MODNAME_ACTUAL
|
||||
PC_FILENAME_VALID = "pelicanconf-valid"
|
||||
PC_FILENAME_SYNTAX_ERROR = "pelicanconf-syntax-error"
|
||||
PC_FILENAME_SYNTAX2_ERROR = "pelicanconf-syntax-error2"
|
||||
|
||||
# FULLNAME_: filename + extension
|
||||
PC_FILENAME_DEFAULT: str = PC_FILENAME_DEFAULT + EXT_PYTHON
|
||||
PC_FULLNAME_VALID: str = PC_FILENAME_VALID + EXT_PYTHON
|
||||
PC_FULLNAME_SYNTAX_ERROR: str = PC_FILENAME_SYNTAX_ERROR + EXT_PYTHON
|
||||
PC_FULLNAME_SYNTAX2_ERROR: str = PC_FILENAME_SYNTAX2_ERROR + EXT_PYTHON
|
||||
|
||||
# Unit Test Case - Syntax Error attributes
|
||||
UT_SYNTAX_ERROR_LINENO = 5
|
||||
UT_SYNTAX_ERROR_OFFSET = 1
|
||||
UT_SYNTAX_ERROR2_LINENO = 13
|
||||
UT_SYNTAX_ERROR2_OFFSET = 5
|
||||
|
||||
logging.basicConfig(level=0)
|
||||
log = logging.getLogger(__name__)
|
||||
logging.root.setLevel(logging.DEBUG)
|
||||
log.propagate = True
|
||||
|
||||
|
||||
# Note: Unittest test setUp/tearDown got replaced by Pytest and its fixtures.
|
||||
#
|
||||
# Pytest provides four levels of fixture scopes:
|
||||
#
|
||||
# * Function (Set up and tear down once for each test function)
|
||||
# * Class (Set up and tear down once for each test class)
|
||||
# * Module (Set up and tear down once for each test module/file)
|
||||
# * Session (Set up and tear down once for each test session i.e. comprising
|
||||
# one or more test files)
|
||||
#
|
||||
# The order of `def` fixtures/functions declarations within a source file
|
||||
# does not matter, all `def`s can be in forward-reference order or
|
||||
# backward-referencable.
|
||||
#
|
||||
# Weird thing about putting fixture(s) inside a function/procedure argument list
|
||||
# is that the ordering of its argument DOES NOT matter: this is a block programming
|
||||
# thing, not like most procedural programming languages.
|
||||
#
|
||||
# To see collection/ordering of fixtures, execute:
|
||||
#
|
||||
# pytest -n0 --setup-plan \
|
||||
# test_settings_config.py::TestSettingsConfig::test_cs_abs_tmpfile
|
||||
#
|
||||
#
|
||||
# Using class in pytest is a way of aggregating similar test cases together.
|
||||
class TestSettingsLoadSourcePath:
|
||||
"""load_source"""
|
||||
|
||||
# Provided a file, it should read it, replace the default values,
|
||||
# append new values to the settings (if any), and apply basic settings
|
||||
# optimizations.
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def fixture_module_get_tests_dir_abs_path(self):
|
||||
"""Get the absolute directory path of `tests` subdirectory
|
||||
|
||||
This pytest module-wide fixture will provide a full directory
|
||||
path of this `test_settings_config.py`.
|
||||
|
||||
Note: used to assist in locating the `settings` directory underneath it.
|
||||
|
||||
This fixture gets evoked exactly once (file-wide) due to `scope=module`.
|
||||
|
||||
:return: Returns the Path of the tests directory
|
||||
:rtype: pathlib.Path"""
|
||||
abs_tests_dirpath: Path = Path(__file__).parent # secret sauce
|
||||
return abs_tests_dirpath
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def fixture_cls_get_settings_dir_abs_path(
|
||||
self, fixture_module_get_tests_dir_abs_path
|
||||
) -> Path:
|
||||
"""Get the absolute directory path of `tests/settings` subdirectory
|
||||
|
||||
This pytest class-wide fixture will provide the full directory
|
||||
path of the `settings` subdirectory containing all the pelicanconf.py files.
|
||||
|
||||
This fixture gets evoked exactly once within its entire class due
|
||||
to `scope=class`.
|
||||
|
||||
:return: Returns the Path of the tests directory
|
||||
:rtype: pathlib.Path"""
|
||||
settings_dirpath: Path = fixture_module_get_tests_dir_abs_path / "settings"
|
||||
return settings_dirpath
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def fixture_func_create_tmp_dir_abs_path(
|
||||
self,
|
||||
fixture_cls_get_settings_dir_abs_path,
|
||||
# redundant to specify other dependencies of sub-fixtures here such as:
|
||||
# fixture_cls_get_settings_dir_abs_path
|
||||
):
|
||||
"""Template the temporary directory
|
||||
|
||||
This pytest function-wide fixture will provide the template name of
|
||||
the temporary directory.
|
||||
|
||||
This fixture executes exactly once every time a test case function references
|
||||
this via `scope=function`."""
|
||||
temporary_dir_path: Path = Path(
|
||||
tempfile.mkdtemp(
|
||||
dir=fixture_cls_get_settings_dir_abs_path, suffix=TMP_DIRNAME_SUFFIX
|
||||
)
|
||||
)
|
||||
# An insurance policy in case a unit test modified the temporary_dir_path var.
|
||||
original_tmp_dir_path = copy.deepcopy(temporary_dir_path)
|
||||
|
||||
yield temporary_dir_path
|
||||
|
||||
shutil.rmtree(original_tmp_dir_path)
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def fixture_func_ut_wrap(self, fixture_func_create_tmp_dir_abs_path):
|
||||
"""Unit test wrapper"""
|
||||
# old setUp() portion
|
||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
# each unit test will do the reading of settings
|
||||
self.DIRSPEC_ABSOLUTE_TMP = fixture_func_create_tmp_dir_abs_path
|
||||
|
||||
yield
|
||||
# old tearDown() portion
|
||||
locale.setlocale(locale.LC_ALL, self.old_locale)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def inject_fixtures(self, caplog):
|
||||
self._caplog = caplog
|
||||
|
||||
# Emptiness
|
||||
def test_load_source_arg_missing_fail(self):
|
||||
"""missing arguments; failing mode"""
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source() # noqa: RUF100
|
||||
assert sample.type == TypeError
|
||||
# assert sample.value.code only exists for SystemExit
|
||||
|
||||
def test_load_source_path_str_blank_fail(self):
|
||||
"""blank string argument; failing mode"""
|
||||
module_type = load_source("", "")
|
||||
assert module_type is None
|
||||
|
||||
def test_load_source_path_arg_str_blank_fail(self):
|
||||
"""argument name with blank str; failing mode"""
|
||||
module_type = load_source(name="", path="")
|
||||
assert module_type is None
|
||||
|
||||
def test_load_source_wrong_arg_fail(self):
|
||||
"""wrong argument name (variant 1); failing mode"""
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(no_such_arg="reject this") # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
# assert sample.value.code only exists for SystemExit
|
||||
|
||||
def test_load_source_arg_unexpected_fail(self):
|
||||
"""wrong argument name (variant 2), failing mode"""
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(pathway="reject this") # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
# assert sample.value.code only exists for SystemExit
|
||||
|
||||
# Module Names, Oh My!
|
||||
def test_load_source_module_arg_unexpected_list_fail(self):
|
||||
"""invalid dict argument type; failing mode"""
|
||||
module_list = {}
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(module_name=module_list) # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
|
||||
def test_load_source_module_path_arg_missing_fail(self):
|
||||
"""invalid list argument type; failing mode"""
|
||||
module_str = ""
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(module_name=module_str) # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
# assert sample.value.code only exists for SystemExit
|
||||
|
||||
# All About The Paths
|
||||
def test_load_source_path_unexpected_type_list_fail(self):
|
||||
"""invalid dict argument type with argument name; failing mode"""
|
||||
path_list = {}
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(path=path_list) # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
|
||||
def test_load_source_path_unexpected_type_dict_fail(self):
|
||||
"""invalid list argument type w/ argument name=; failing mode"""
|
||||
path_dict = []
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(path=path_dict) # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
|
||||
def test_load_source_path_unexpected_type_tuple_fail(self):
|
||||
"""invalid tuple argument type w/ argument name=; failing mode"""
|
||||
path_tuple = ()
|
||||
with pytest.raises(TypeError) as sample:
|
||||
load_source(path=path_tuple) # NOQA: RUF100
|
||||
assert sample.type == TypeError
|
||||
|
||||
def test_load_source_path_valid_pelicanconf_py_pass(self):
|
||||
"""correct working function call; passing mode"""
|
||||
path: str = DIRSPEC_RELATIVE + PC_FULLNAME_VALID
|
||||
module_type = load_source(name="", path=path) # extract module name from file
|
||||
assert module_type is not None
|
||||
|
||||
# @log_function_details
|
||||
def test_load_source_path_pelicanconf_abs_syntax_error_fail(
|
||||
self,
|
||||
fixture_func_create_tmp_dir_abs_path,
|
||||
fixture_cls_get_settings_dir_abs_path,
|
||||
):
|
||||
"""syntax error; absolute path; str type; failing mode"""
|
||||
datadir_path = fixture_cls_get_settings_dir_abs_path
|
||||
tmp_path = fixture_func_create_tmp_dir_abs_path
|
||||
ro_filename = PC_FULLNAME_SYNTAX_ERROR + EXT_PYTHON_DISABLED
|
||||
src_ro_filespec: Path = datadir_path / ro_filename
|
||||
file_under_unit_test_filespec: Path = tmp_path / PC_FULLNAME_SYNTAX_ERROR
|
||||
|
||||
# Copy mangled pseudo-Python file into temporary area as a Python file
|
||||
shutil.copyfile(src_ro_filespec, file_under_unit_test_filespec)
|
||||
|
||||
with self._caplog.at_level(logging.DEBUG):
|
||||
with pytest.raises(SyntaxError) as sample:
|
||||
self._caplog.clear()
|
||||
|
||||
load_source(path=str(file_under_unit_test_filespec), name="") # NOQA: RUF100
|
||||
# ignore return value due to SyntaxError exception
|
||||
|
||||
assert "unexpected indent" in self._caplog.text
|
||||
assert sample.type == SyntaxError
|
||||
assert sample.value.args[1]["lineno"] == UT_SYNTAX_ERROR_LINENO
|
||||
assert sample.value.args[1]["offset"] == UT_SYNTAX_ERROR_OFFSET
|
||||
|
||||
Path(file_under_unit_test_filespec).unlink(missing_ok=True)
|
||||
|
||||
def test_load_source_path_pelicanconf_abs_syntax_error2_fail(
|
||||
self,
|
||||
fixture_func_create_tmp_dir_abs_path,
|
||||
fixture_cls_get_settings_dir_abs_path,
|
||||
):
|
||||
"""syntax error; absolute path; str type; failing mode"""
|
||||
datadir_path = fixture_cls_get_settings_dir_abs_path
|
||||
tmp_path = fixture_func_create_tmp_dir_abs_path
|
||||
ro_filename = PC_FULLNAME_SYNTAX2_ERROR + EXT_PYTHON_DISABLED
|
||||
src_ro_filespec: Path = datadir_path / ro_filename
|
||||
file_under_unit_test_filespec: Path = tmp_path / PC_FULLNAME_SYNTAX2_ERROR
|
||||
|
||||
# Copy mangled pseudo-Python file into temporary area as a Python file
|
||||
shutil.copyfile(src_ro_filespec, file_under_unit_test_filespec)
|
||||
|
||||
with self._caplog.at_level(logging.DEBUG):
|
||||
with pytest.raises(SyntaxError) as sample:
|
||||
self._caplog.clear()
|
||||
|
||||
load_source(path=str(file_under_unit_test_filespec), name="") # NOQA: RUF100
|
||||
# ignore return value due to SyntaxError exception
|
||||
|
||||
assert "Invalid syntax" in self._caplog.text
|
||||
assert sample.type == SyntaxError
|
||||
assert sample.value.args[1]["lineno"] == UT_SYNTAX_ERROR2_LINENO
|
||||
assert sample.value.args[1]["offset"] == UT_SYNTAX_ERROR2_OFFSET
|
||||
|
||||
Path(file_under_unit_test_filespec).unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# if executing this file alone, it tests this file alone.
|
||||
# Can execute from any current working directory
|
||||
pytest.main([__file__])
|
||||
|
||||
# more, complex variants of pytest.
|
||||
# pytest.main([__file__, "-n0", "-rAw", "--capture=no", "--no-header"])
|
||||
# pytest.main([__file__, "-n0"]) # single-process, single-thread
|
||||
Loading…
Add table
Add a link
Reference in a new issue