From 77f1552276b773f7c4467fa865d2cc48d2b35a26 Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Tue, 1 Sep 2026 15:27:53 +0300 Subject: [PATCH] Fix three stale documented defaults and add a check for them TopoJson.overlay, TimeSliderChoropleth.overlay and GeoJsonPopup.localize each override a default and kept the parameter description they were copied from, so the docstring states the old value: TopoJson.overlay documented False, defaults True TimeSliderChoropleth.overlay documented False, defaults True GeoJsonPopup.localize documented False, defaults True The code is right in each case. Layer documents and defaults overlay=False and GeoJson correctly documents its True override; GeoJsonTooltip correctly documents localize=False. Only these three drifted. The new test walks every class in the package and compares the documented boolean default against the signature default for overlay, control, show and localize. --- folium/features.py | 4 +- folium/plugins/time_slider_choropleth.py | 2 +- tests/test_docstring_defaults.py | 86 ++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/test_docstring_defaults.py diff --git a/folium/features.py b/folium/features.py index b598afd062..2303ef3a36 100644 --- a/folium/features.py +++ b/folium/features.py @@ -950,7 +950,7 @@ class TopoJson(JSCSSMixin, Layer): A function mapping a TopoJson geometry to a style dict. name : string, default None The name of the Layer, as it will appear in LayerControls - overlay : bool, default False + overlay : bool, default True Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls. @@ -1338,7 +1338,7 @@ class GeoJsonPopup(GeoJsonDetail): instead of the keys of `fields`. labels: bool, default True. Set to False to disable displaying the field names or aliases. - localize: bool, default False. + localize: bool, default True. This will use JavaScript's .toLocaleString() to format 'clean' values as strings for the user's location; i.e. 1,000,000.00 comma separators, float truncation, etc. diff --git a/folium/plugins/time_slider_choropleth.py b/folium/plugins/time_slider_choropleth.py index 26945bb774..fa2a1ac221 100644 --- a/folium/plugins/time_slider_choropleth.py +++ b/folium/plugins/time_slider_choropleth.py @@ -24,7 +24,7 @@ class TimeSliderChoropleth(JSCSSMixin, Layer): Whether to show a visual effect on mouse hover and click. name : string, default None The name of the Layer, as it will appear in LayerControls. - overlay : bool, default False + overlay : bool, default True Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls. diff --git a/tests/test_docstring_defaults.py b/tests/test_docstring_defaults.py new file mode 100644 index 0000000000..7b87d6f107 --- /dev/null +++ b/tests/test_docstring_defaults.py @@ -0,0 +1,86 @@ +""" +Check that documented defaults match the signature defaults. + +Several classes override a default inherited from ``Layer`` or from a sibling +class but keep the parameter description they were copied from, so the +docstring ends up stating the old default. Scoped to the boolean flags whose +documented value is a plain ``True``/``False`` literal, which is what makes the +comparison unambiguous. +""" + +import inspect +import pkgutil +import re +from importlib import import_module + +import pytest + +import folium + +FLAGS = ("overlay", "control", "show", "localize") + + +def _classes(): + modules = [folium] + for mod in pkgutil.walk_packages(folium.__path__, folium.__name__ + "."): + try: + modules.append(import_module(mod.name)) + except ImportError: # optional dependency + continue + seen = set() + for module in modules: + for _, obj in inspect.getmembers(module, inspect.isclass): + if obj.__module__.startswith("folium.") and obj not in seen: + seen.add(obj) + yield obj + + +def _documented_default(obj, flag): + for source in (obj.__init__.__doc__, obj.__doc__): + if not source: + continue + match = re.search( + rf"^\s*{flag}\s*:[^\n]*default[ =:]+(True|False)\b", + source, + re.MULTILINE, + ) + if match: + return match.group(1) == "True" + return None + + +@pytest.mark.parametrize("cls", sorted(_classes(), key=lambda c: c.__qualname__)) +def test_documented_boolean_defaults_match_signature(cls): + try: + signature = inspect.signature(cls.__init__) + except (TypeError, ValueError): + pytest.skip("no introspectable signature") + for flag in FLAGS: + parameter = signature.parameters.get(flag) + if parameter is None or not isinstance(parameter.default, bool): + continue + documented = _documented_default(cls, flag) + if documented is None: + continue + assert documented == parameter.default, ( + f"{cls.__qualname__}.{flag} is documented as default " + f"{documented} but defaults to {parameter.default}" + ) + + +def test_the_check_can_actually_fail(): + """Guard against the check passing because it never reads anything.""" + + class Example: + """ + Parameters + ---------- + overlay : bool, default False + Doc and signature disagree on purpose. + """ + + def __init__(self, overlay: bool = True): + pass + + assert _documented_default(Example, "overlay") is False + assert inspect.signature(Example.__init__).parameters["overlay"].default is True