PYTHON-3036 Improve error message for unknown MongoClient options (#1440)

This commit is contained in:
Casey Clements 2023-11-30 14:21:10 -05:00 committed by GitHub
parent 6537415da7
commit d4dfd4a044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 8 deletions

View File

@ -98,3 +98,4 @@ The following is a list of people who have contributed to
- Dainis Gorbunovs (DainisGorbunovs)
- Iris Ho (sleepyStick)
- Stephan Hof (stephan-hof)
- Casey Clements (caseyclements)

View File

@ -20,6 +20,7 @@ import datetime
import inspect
import warnings
from collections import OrderedDict, abc
from difflib import get_close_matches
from typing import (
TYPE_CHECKING,
Any,
@ -162,9 +163,12 @@ def clean_node(node: str) -> tuple[str, int]:
return host.lower(), port
def raise_config_error(key: str, dummy: Any) -> NoReturn:
def raise_config_error(key: str, suggestions: Optional[list] = None) -> NoReturn:
"""Raise ConfigurationError with the given key name."""
raise ConfigurationError(f"Unknown option {key}")
msg = f"Unknown option: {key}."
if suggestions:
msg += f" Did you mean one of ({', '.join(suggestions)}) or maybe a camelCase version of one? Refer to docstring."
raise ConfigurationError(msg)
# Mapping of URI uuid representation options to valid subtypes.
@ -810,14 +814,24 @@ def validate_auth_option(option: str, value: Any) -> tuple[str, Any]:
"""Validate optional authentication parameters."""
lower, value = validate(option, value)
if lower not in _AUTH_OPTIONS:
raise ConfigurationError(f"Unknown authentication option: {option}")
raise ConfigurationError(f"Unknown option: {option}. Must be in {_AUTH_OPTIONS}")
return option, value
def _get_validator(
key: str, validators: dict[str, Callable[[Any, Any], Any]], normed_key: Optional[str] = None
) -> Callable:
normed_key = normed_key or key
try:
return validators[normed_key]
except KeyError:
suggestions = get_close_matches(normed_key, validators, cutoff=0.2)
raise_config_error(key, suggestions)
def validate(option: str, value: Any) -> tuple[str, Any]:
"""Generic validation function."""
lower = option.lower()
validator = VALIDATORS.get(lower, raise_config_error)
validator = _get_validator(option, VALIDATORS, normed_key=option.lower())
value = validator(option, value)
return option, value
@ -855,15 +869,15 @@ def get_validated_options(
for opt, value in options.items():
normed_key = get_normed_key(opt)
try:
validator = URI_OPTIONS_VALIDATOR_MAP.get(normed_key, raise_config_error)
value = validator(opt, value) # noqa: PLW2901
validator = _get_validator(opt, URI_OPTIONS_VALIDATOR_MAP, normed_key=normed_key)
validated = validator(opt, value)
except (ValueError, TypeError, ConfigurationError) as exc:
if warn:
warnings.warn(str(exc), stacklevel=2)
else:
raise
else:
validated_options[get_setter_key(normed_key)] = value
validated_options[get_setter_key(normed_key)] = validated
return validated_options

View File

@ -21,6 +21,7 @@ import copy
import datetime
import gc
import os
import re
import signal
import socket
import struct
@ -535,6 +536,14 @@ class ClientUnitTest(unittest.TestCase):
self.assertIsInstance(c.options.retry_writes, bool)
self.assertIsInstance(c.options.retry_reads, bool)
def test_validate_suggestion(self):
"""Validate kwargs in constructor."""
for typo in ["auth", "Auth", "AUTH"]:
expected = f"Unknown option: {typo}. Did you mean one of (authsource, authmechanism, authoidcallowedhosts) or maybe a camelCase version of one? Refer to docstring."
expected = re.escape(expected)
with self.assertRaisesRegex(ConfigurationError, expected):
MongoClient(**{typo: "standard"}) # type: ignore[arg-type]
class TestClient(IntegrationTest):
def test_multiple_uris(self):

View File

@ -144,6 +144,12 @@ class TestURI(unittest.TestCase):
self.assertEqual({"authsource": "foobar"}, split_options("authSource=foobar"))
self.assertEqual({"maxpoolsize": 50}, split_options("maxpoolsize=50"))
# Test suggestions given when invalid kwarg passed
expected = r"Unknown option: auth. Did you mean one of \(authsource, authmechanism, timeoutms\) or maybe a camelCase version of one\? Refer to docstring."
with self.assertRaisesRegex(ConfigurationError, expected):
split_options("auth=GSSAPI")
def test_parse_uri(self):
self.assertRaises(InvalidURI, parse_uri, "http://foobar.com")
self.assertRaises(InvalidURI, parse_uri, "http://foo@foobar.com")