Skip to content

Commit d4dfd4a

Browse files
PYTHON-3036 Improve error message for unknown MongoClient options (mongodb#1440)
1 parent 6537415 commit d4dfd4a

File tree

4 files changed

+38
-8
lines changed

4 files changed

+38
-8
lines changed

doc/contributors.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ The following is a list of people who have contributed to
9898
- Dainis Gorbunovs (DainisGorbunovs)
9999
- Iris Ho (sleepyStick)
100100
- Stephan Hof (stephan-hof)
101+
- Casey Clements (caseyclements)

pymongo/common.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import inspect
2121
import warnings
2222
from collections import OrderedDict, abc
23+
from difflib import get_close_matches
2324
from typing import (
2425
TYPE_CHECKING,
2526
Any,
@@ -162,9 +163,12 @@ def clean_node(node: str) -> tuple[str, int]:
162163
return host.lower(), port
163164

164165

165-
def raise_config_error(key: str, dummy: Any) -> NoReturn:
166+
def raise_config_error(key: str, suggestions: Optional[list] = None) -> NoReturn:
166167
"""Raise ConfigurationError with the given key name."""
167-
raise ConfigurationError(f"Unknown option {key}")
168+
msg = f"Unknown option: {key}."
169+
if suggestions:
170+
msg += f" Did you mean one of ({', '.join(suggestions)}) or maybe a camelCase version of one? Refer to docstring."
171+
raise ConfigurationError(msg)
168172

169173

170174
# Mapping of URI uuid representation options to valid subtypes.
@@ -810,14 +814,24 @@ def validate_auth_option(option: str, value: Any) -> tuple[str, Any]:
810814
"""Validate optional authentication parameters."""
811815
lower, value = validate(option, value)
812816
if lower not in _AUTH_OPTIONS:
813-
raise ConfigurationError(f"Unknown authentication option: {option}")
817+
raise ConfigurationError(f"Unknown option: {option}. Must be in {_AUTH_OPTIONS}")
814818
return option, value
815819

816820

821+
def _get_validator(
822+
key: str, validators: dict[str, Callable[[Any, Any], Any]], normed_key: Optional[str] = None
823+
) -> Callable:
824+
normed_key = normed_key or key
825+
try:
826+
return validators[normed_key]
827+
except KeyError:
828+
suggestions = get_close_matches(normed_key, validators, cutoff=0.2)
829+
raise_config_error(key, suggestions)
830+
831+
817832
def validate(option: str, value: Any) -> tuple[str, Any]:
818833
"""Generic validation function."""
819-
lower = option.lower()
820-
validator = VALIDATORS.get(lower, raise_config_error)
834+
validator = _get_validator(option, VALIDATORS, normed_key=option.lower())
821835
value = validator(option, value)
822836
return option, value
823837

@@ -855,15 +869,15 @@ def get_setter_key(x: str) -> str:
855869
for opt, value in options.items():
856870
normed_key = get_normed_key(opt)
857871
try:
858-
validator = URI_OPTIONS_VALIDATOR_MAP.get(normed_key, raise_config_error)
859-
value = validator(opt, value) # noqa: PLW2901
872+
validator = _get_validator(opt, URI_OPTIONS_VALIDATOR_MAP, normed_key=normed_key)
873+
validated = validator(opt, value)
860874
except (ValueError, TypeError, ConfigurationError) as exc:
861875
if warn:
862876
warnings.warn(str(exc), stacklevel=2)
863877
else:
864878
raise
865879
else:
866-
validated_options[get_setter_key(normed_key)] = value
880+
validated_options[get_setter_key(normed_key)] = validated
867881
return validated_options
868882

869883

test/test_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import datetime
2222
import gc
2323
import os
24+
import re
2425
import signal
2526
import socket
2627
import struct
@@ -535,6 +536,14 @@ def test_client_options(self):
535536
self.assertIsInstance(c.options.retry_writes, bool)
536537
self.assertIsInstance(c.options.retry_reads, bool)
537538

539+
def test_validate_suggestion(self):
540+
"""Validate kwargs in constructor."""
541+
for typo in ["auth", "Auth", "AUTH"]:
542+
expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring."
543+
expected = re.escape(expected)
544+
with self.assertRaisesRegex(ConfigurationError, expected):
545+
MongoClient(**{typo: "standard"}) # type: ignore[arg-type]
546+
538547

539548
class TestClient(IntegrationTest):
540549
def test_multiple_uris(self):

test/test_uri_parser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ def test_split_options(self):
144144
self.assertEqual({"authsource": "foobar"}, split_options("authSource=foobar"))
145145
self.assertEqual({"maxpoolsize": 50}, split_options("maxpoolsize=50"))
146146

147+
# Test suggestions given when invalid kwarg passed
148+
149+
expected = r"Unknown option: auth. Did you mean one of \(authsource, authmechanism, timeoutms\) or maybe a camelCase version of one\? Refer to docstring."
150+
with self.assertRaisesRegex(ConfigurationError, expected):
151+
split_options("auth=GSSAPI")
152+
147153
def test_parse_uri(self):
148154
self.assertRaises(InvalidURI, parse_uri, "http://foobar.com")
149155
self.assertRaises(InvalidURI, parse_uri, "http://foo@foobar.com")

0 commit comments

Comments
 (0)