Repository: geopy/geopy
Pull request #291 (2018-05-10 07:35:25)

Add ssl context support
Adds the global `geopy.geocoders.options.default_ssl_context` setting and `ssl_context` param to each geocoder.

By specifying a custom TLS context it's possible to disable TLS verification or use specific CA certificates.

For older versions of Python (before 2.7.9 and 3.4.3) this would issue a warning about missing TLS verification support (even for `http` schemes):
```
UserWarning: SSL context is not supported in your environment for urllib calls. Perhaps your Python version is obsolete? This probably means that TLS verification doesn't happen, which is insecure. Please consider upgrading your Python interpreter version.
```

Usage for disabling TLS verification completely:
```
import ssl
import geopy.geocoders
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
geopy.geocoders.options.default_ssl_context = ctx

from geopy.geocoders import Nominatim
geolocator = Nominatim()
geolocator.geocode("175 5th Avenue NYC")
```

The following issues should be resolved by this PR: #124 #237 #212 

This issue is from the current roadmap #267.


Patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6c875f5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.py]
+indent_size = 4
+# Docstrings and comments should have max_line_length = 80.
+max_line_length = 90
+
+[*.{md,rst}]
+trim_trailing_whitespace = false
+max_line_length = 80
+
+[Makefile]
+indent_style = tab
diff --git a/geopy/compat.py b/geopy/compat.py
index 3f310ee..36f33fd 100644
--- a/geopy/compat.py
+++ b/geopy/compat.py
@@ -2,7 +2,9 @@
 Compatibility...
 """
 
+import inspect
 import sys
+import warnings
 
 py3k = sys.version_info >= (3, 0)
 
@@ -50,7 +52,7 @@ if py3k: # pragma: no cover
     from urllib.parse import (urlencode, quote, # pylint: disable=W0611,F0401,W0611,E0611
                               urlparse, parse_qs)
     from urllib.request import (Request, urlopen, # pylint: disable=W0611,F0401,W0611,E0611
-                                build_opener, ProxyHandler,
+                                build_opener, ProxyHandler, HTTPSHandler,
                                 URLError,
                                 HTTPPasswordMgrWithDefaultRealm,
                                 HTTPBasicAuthHandler)
@@ -74,7 +76,7 @@ if py3k: # pragma: no cover
 else: # pragma: no cover
     from urllib import urlencode as original_urlencode, quote # pylint: disable=W0611,F0401,W0611,E0611
     from urllib2 import (Request, HTTPError,   # pylint: disable=W0611,F0401,W0611,E0611
-                         ProxyHandler, URLError, urlopen,
+                         ProxyHandler, HTTPSHandler, URLError, urlopen,
                          build_opener,
                          HTTPPasswordMgrWithDefaultRealm,
                          HTTPBasicAuthHandler)
@@ -120,3 +122,31 @@ else: # pragma: no cover
         For Python3
         """
         return d.iteritems()
+
+
+def _is_urllib_context_supported(HTTPSHandler_=HTTPSHandler):
+    context_arg = 'context'
+    if py3k:
+        argspec = inspect.getfullargspec(HTTPSHandler_.__init__)
+        return context_arg in argspec.args or context_arg in argspec.kwonlyargs
+    else:
+        return context_arg in inspect.getargspec(HTTPSHandler_.__init__).args
+
+
+_URLLIB_SUPPORTS_SSL_CONTEXT = _is_urllib_context_supported()
+
+
+def build_opener_with_context(context=None, *handlers):
+    # `context` has been added in Python 2.7.9 and 3.4.3.
+    if _URLLIB_SUPPORTS_SSL_CONTEXT:
+        https_handler = HTTPSHandler(context=context)
+    else:
+        warnings.warn(
+            ("SSL context is not supported in your environment for urllib "
+             "calls. Perhaps your Python version is obsolete? "
+             "This probably means that TLS verification doesn't happen, "
+             "which is insecure. Please consider upgrading your Python "
+             "interpreter version."),
+            UserWarning)
+        https_handler = HTTPSHandler()
+    return build_opener(https_handler, *handlers)
diff --git a/geopy/geocoders/arcgis.py b/geopy/geocoders/arcgis.py
index 0fad66e..33b8758 100644
--- a/geopy/geocoders/arcgis.py
+++ b/geopy/geocoders/arcgis.py
@@ -41,6 +41,7 @@ class ArcGIS(Geocoder):  # pylint: disable=R0921,R0902,W0223
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Create a ArcGIS-based geocoder.
@@ -78,6 +79,12 @@ class ArcGIS(Geocoder):  # pylint: disable=R0921,R0902,W0223
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(ArcGIS, self).__init__(
@@ -86,6 +93,7 @@ class ArcGIS(Geocoder):  # pylint: disable=R0921,R0902,W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         if username or password or referer:
             if not (username and password and referer):
diff --git a/geopy/geocoders/baidu.py b/geopy/geocoders/baidu.py
index 44ba95f..b1f549f 100644
--- a/geopy/geocoders/baidu.py
+++ b/geopy/geocoders/baidu.py
@@ -29,6 +29,7 @@ class Baidu(Geocoder):
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a customized Baidu geocoder using the v2 API.
@@ -59,6 +60,12 @@ class Baidu(Geocoder):
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(Baidu, self).__init__(
@@ -67,6 +74,7 @@ class Baidu(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.api = '%s://api.map.baidu.com/geocoder/v2/' % self.scheme
diff --git a/geopy/geocoders/base.py b/geopy/geocoders/base.py
index da0b622..cfa4371 100644
--- a/geopy/geocoders/base.py
+++ b/geopy/geocoders/base.py
@@ -4,6 +4,7 @@
 
 from ssl import SSLError
 from socket import timeout as SocketTimeout
+import functools
 import json
 import warnings
 
@@ -11,8 +12,7 @@ from geopy.compat import (
     string_compare,
     HTTPError,
     py3k,
-    urlopen as urllib_urlopen,
-    build_opener,
+    build_opener_with_context,
     ProxyHandler,
     URLError,
     Request,
@@ -85,6 +85,28 @@ class options(object):
     default_user_agent = "geopy/%s" % __version__
     """User-Agent header to send with the requests to geocoder API."""
 
+    default_ssl_context = None
+    """An :class:`ssl.SSLContext` instance with custom TLS verification
+    settings. Pass ``None`` to use the interpreter's defaults (starting from
+    Python 2.7.9 and 3.4.3 that is to use the system's trusted CA certificates;
+    the older versions don't support TLS verification completely).
+
+    For older versions of Python (before 2.7.9 and 3.4.3) this argument is
+    ignored, as ``urlopen`` doesn't accept an ssl context there, and a warning
+    is issued.
+
+    To disable TLS certificate verification completely::
+
+        >>> import ssl
+        >>> import geopy.geocoders
+        >>> ctx = ssl.create_default_context()
+        >>> ctx.check_hostname = False
+        >>> ctx.verify_mode = ssl.CERT_NONE
+        >>> geopy.geocoders.options.default_ssl_context = ctx
+
+    See docs for the :class:`ssl.SSLContext` class for more examples.
+    """
+
 
 # Create an object which `repr` returns 'DEFAULT_SENTINEL'. Sphinx (docs) uses
 # this value when generating method's signature.
@@ -118,6 +140,7 @@ class Geocoder(object):
             timeout=DEFAULT_SENTINEL,
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Mostly-common geocoder validation, proxies, &c. Not all geocoders
@@ -134,14 +157,19 @@ class Geocoder(object):
         self.proxies = (proxies if proxies is not DEFAULT_SENTINEL
                         else options.default_proxies)
         self.headers = {'User-Agent': user_agent or options.default_user_agent}
+        self.ssl_context = (ssl_context if ssl_context is not DEFAULT_SENTINEL
+                            else options.default_ssl_context)
 
         if self.proxies:
-            opener = build_opener(
-                ProxyHandler(self.proxies)
+            opener = build_opener_with_context(
+                self.ssl_context,
+                ProxyHandler(self.proxies),
             )
-            self.urlopen = opener.open
         else:
-            self.urlopen = urllib_urlopen
+            opener = build_opener_with_context(
+                self.ssl_context,
+            )
+        self.urlopen = opener.open
 
     @staticmethod
     def _coerce_point_to_string(point):
@@ -181,8 +209,9 @@ class Geocoder(object):
         """
 
         if requester:
-            # Don't construct an urllib's Request for a custom requester
-            req = url
+            req = url  # Don't construct an urllib's Request for a custom requester
+            requester = functools.partial(requester, context=self.ssl_context,
+                                          headers=self.headers)
         else:
             if isinstance(url, Request):
                 # copy Request
diff --git a/geopy/geocoders/bing.py b/geopy/geocoders/bing.py
index 67c88c8..c8daf59 100644
--- a/geopy/geocoders/bing.py
+++ b/geopy/geocoders/bing.py
@@ -39,6 +39,7 @@ class Bing(Geocoder):
             timeout=DEFAULT_SENTINEL,
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """Initialize a customized Bing geocoder with location-specific
         address information and your Bing Maps API key.
@@ -61,6 +62,12 @@ class Bing(Geocoder):
             See :attr:`geopy.geocoders.options.default_user_agent`.
 
             .. versionadded:: 1.12.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
         super(Bing, self).__init__(
             format_string=format_string,
@@ -68,6 +75,7 @@ class Bing(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.api = "%s://dev.virtualearth.net/REST/v1/Locations" % self.scheme
diff --git a/geopy/geocoders/databc.py b/geopy/geocoders/databc.py
index 1bdd3ab..01fdbda 100644
--- a/geopy/geocoders/databc.py
+++ b/geopy/geocoders/databc.py
@@ -24,6 +24,7 @@ class DataBC(Geocoder):
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Create a DataBC-based geocoder.
@@ -45,6 +46,12 @@ class DataBC(Geocoder):
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(DataBC, self).__init__(
@@ -53,6 +60,7 @@ class DataBC(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api = '%s://apps.gov.bc.ca/pub/geocoder/addresses.geojson' % self.scheme
 
diff --git a/geopy/geocoders/geocodefarm.py b/geopy/geocoders/geocodefarm.py
index 84f48cb..1bcba10 100644
--- a/geopy/geocoders/geocodefarm.py
+++ b/geopy/geocoders/geocodefarm.py
@@ -28,6 +28,7 @@ class GeocodeFarm(Geocoder):
             timeout=DEFAULT_SENTINEL,
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Create a geocoder for GeocodeFarm.
@@ -48,6 +49,12 @@ class GeocodeFarm(Geocoder):
             See :attr:`geopy.geocoders.options.default_user_agent`.
 
             .. versionadded:: 1.12.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
         super(GeocodeFarm, self).__init__(
             format_string=format_string,
@@ -55,6 +62,7 @@ class GeocodeFarm(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.api = (
diff --git a/geopy/geocoders/geonames.py b/geopy/geocoders/geonames.py
index 1e8c41b..3649de1 100644
--- a/geopy/geocoders/geonames.py
+++ b/geopy/geocoders/geonames.py
@@ -1,6 +1,7 @@
 """
 :class:`GeoNames` geocoder.
 """
+import warnings
 
 from geopy.compat import urlencode
 from geopy.exc import (
@@ -32,6 +33,7 @@ class GeoNames(Geocoder): # pylint: disable=W0223
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         :param str country_bias:
@@ -53,6 +55,12 @@ class GeoNames(Geocoder): # pylint: disable=W0223
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(GeoNames, self).__init__(
@@ -61,6 +69,7 @@ class GeoNames(Geocoder): # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         if username is None:
             raise ConfigurationError(
@@ -107,7 +116,7 @@ class GeoNames(Geocoder): # pylint: disable=W0223
     def reverse(
             self,
             query,
-            exactly_one=False,
+            exactly_one=DEFAULT_SENTINEL,
             timeout=DEFAULT_SENTINEL,
     ):
         """
@@ -123,12 +132,26 @@ class GeoNames(Geocoder): # pylint: disable=W0223
         :param bool exactly_one: Return one result or a list of results, if
             available.
 
+            .. versionchanged:: 1.14.0
+               Default value for ``exactly_one`` was ``False``, which differs
+               from the conventional default across geopy. Please always pass
+               this argument explicitly, otherwise you would get a warning.
+               In geopy 2.0 the default value will become ``True``.
+
         :param int timeout: Time, in seconds, to wait for the geocoding service
             to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
             exception. Set this only if you wish to override, on this call
             only, the value set during the geocoder's initialization.
 
         """
+        if exactly_one is DEFAULT_SENTINEL:
+            warnings.warn('%s.reverse: default value for `exactly_one` '
+                          'argument will become True in geopy 2.0. '
+                          'Specify `exactly_one=False` as the argument '
+                          'explicitly to get rid of this warning.' % type(self).__name__,
+                          DeprecationWarning)
+            exactly_one = False
+
         try:
             lat, lng = [
                 x.strip() for x in
diff --git a/geopy/geocoders/googlev3.py b/geopy/geocoders/googlev3.py
index e7f01c4..bebc55b 100644
--- a/geopy/geocoders/googlev3.py
+++ b/geopy/geocoders/googlev3.py
@@ -5,6 +5,7 @@
 import base64
 import hashlib
 import hmac
+import warnings
 
 from geopy.compat import urlencode
 from geopy.exc import (
@@ -47,6 +48,7 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
             channel='',
     ):
         """
@@ -86,6 +88,12 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
 
             .. versionadded:: 1.14.0
 
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
+
         :param str channel: If using premier, the channel identifier.
 
             .. versionadded:: 1.12.0
@@ -96,6 +104,7 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         if client_id and not secret_key:
             raise ConfigurationError('Must provide secret_key with client_id.')
@@ -159,7 +168,7 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
 
     def geocode(
             self,
-            query,
+            query=None,
             exactly_one=True,
             timeout=DEFAULT_SENTINEL,
             bounds=None,
@@ -171,7 +180,14 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
         """
         Geocode a location query.
 
-        :param str query: The address or query you wish to geocode.
+        :param str query: The address or query you wish to geocode. Optional,
+            if ``components`` param is set::
+
+                >>> g = GoogleV3()
+                >>> g.geocode(components={"city": "Paris", "country": "FR"})
+
+            .. versionchanged:: 1.14.0
+               Now ``query`` is optional if ``components`` param is set.
 
         :param bool exactly_one: Return one result or a list of results, if
             available.
@@ -197,9 +213,12 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
             device with a location sensor.
         """
         params = {
-            'address': self.format_string % query,
             'sensor': str(sensor).lower()
         }
+        if query is None and not components:
+            raise ValueError('Either `query` or `components` must be set.`')
+        if query is not None:
+            params['address'] = self.format_string % query
         if self.api_key:
             params['key'] = self.api_key
         if bounds:
@@ -228,7 +247,7 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
     def reverse(
             self,
             query,
-            exactly_one=False,
+            exactly_one=DEFAULT_SENTINEL,
             timeout=DEFAULT_SENTINEL,
             language=None,
             sensor=False,
@@ -244,6 +263,12 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
         :param bool exactly_one: Return one result or a list of results, if
             available.
 
+            .. versionchanged:: 1.14.0
+               Default value for ``exactly_one`` was ``False``, which differs
+               from the conventional default across geopy. Please always pass
+               this argument explicitly, otherwise you would get a warning.
+               In geopy 2.0 the default value will become ``True``.
+
         :param int timeout: Time, in seconds, to wait for the geocoding service
             to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
             exception. Set this only if you wish to override, on this call
@@ -254,6 +279,14 @@ class GoogleV3(Geocoder):  # pylint: disable=R0902
         :param bool sensor: Whether the geocoding request comes from a
             device with a location sensor.
         """
+        if exactly_one is DEFAULT_SENTINEL:
+            warnings.warn('%s.reverse: default value for `exactly_one` '
+                          'argument will become True in geopy 2.0. '
+                          'Specify `exactly_one=False` as the argument '
+                          'explicitly to get rid of this warning.' % type(self).__name__,
+                          DeprecationWarning)
+            exactly_one = False
+
         params = {
             'latlng': self._coerce_point_to_string(query),
             'sensor': str(sensor).lower()
diff --git a/geopy/geocoders/ignfrance.py b/geopy/geocoders/ignfrance.py
index d6bd2c7..7126579 100644
--- a/geopy/geocoders/ignfrance.py
+++ b/geopy/geocoders/ignfrance.py
@@ -1,7 +1,7 @@
 """
 :class:`.IGNFrance` is the IGN France Geocoder.
 """
-
+import warnings
 import xml.etree.ElementTree as ET
 
 from geopy.compat import (HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm, Request,
@@ -48,6 +48,7 @@ class IGNFrance(Geocoder):   # pylint: disable=W0223
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a customized IGN France geocoder.
@@ -89,6 +90,12 @@ class IGNFrance(Geocoder):   # pylint: disable=W0223
 
             .. versionadded:: 1.14.0
 
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
+
         """
         super(IGNFrance, self).__init__(
             format_string=format_string,
@@ -96,6 +103,7 @@ class IGNFrance(Geocoder):   # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
 
         # Catch if no api key with username and password
@@ -238,7 +246,7 @@ class IGNFrance(Geocoder):   # pylint: disable=W0223
             reverse_geocode_preference=('StreetAddress', ),
             maximum_responses=25,
             filtering='',
-            exactly_one=False,
+            exactly_one=DEFAULT_SENTINEL,
             timeout=DEFAULT_SENTINEL,
     ):
         """
@@ -263,12 +271,25 @@ class IGNFrance(Geocoder):   # pylint: disable=W0223
         :param bool exactly_one: Return one result or a list of results, if
             available.
 
+            .. versionchanged:: 1.14.0
+               Default value for ``exactly_one`` was ``False``, which differs
+               from the conventional default across geopy. Please always pass
+               this argument explicitly, otherwise you would get a warning.
+               In geopy 2.0 the default value will become ``True``.
+
         :param int timeout: Time, in seconds, to wait for the geocoding service
             to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
             exception. Set this only if you wish to override, on this call
             only, the value set during the geocoder's initialization.
 
         """
+        if exactly_one is DEFAULT_SENTINEL:
+            warnings.warn('%s.reverse: default value for `exactly_one` '
+                          'argument will become True in geopy 2.0. '
+                          'Specify `exactly_one=False` as the argument '
+                          'explicitly to get rid of this warning.' % type(self).__name__,
+                          DeprecationWarning)
+            exactly_one = False
 
         sub_request = """
             <ReverseGeocodeRequest>
diff --git a/geopy/geocoders/mapzen.py b/geopy/geocoders/mapzen.py
index 2a3f500..8a13b86 100644
--- a/geopy/geocoders/mapzen.py
+++ b/geopy/geocoders/mapzen.py
@@ -31,6 +31,7 @@ class Mapzen(Geocoder):
             user_agent=None,
             domain='search.mapzen.com',
             scheme=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         :param str api_key: Mapzen API key, optional.
@@ -59,6 +60,12 @@ class Mapzen(Geocoder):
         :param str scheme:
             See :attr:`geopy.geocoders.options.default_scheme`.
 
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
+
         """
         super(Mapzen, self).__init__(
             format_string=format_string,
@@ -66,6 +73,7 @@ class Mapzen(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.country_bias = country_bias
         self.boundary_rect = boundary_rect
diff --git a/geopy/geocoders/opencage.py b/geopy/geocoders/opencage.py
index f5ad0ea..de1fcd6 100644
--- a/geopy/geocoders/opencage.py
+++ b/geopy/geocoders/opencage.py
@@ -1,6 +1,7 @@
 """
 :class:`.OpenCage` is the Opencagedata geocoder.
 """
+import warnings
 
 from geopy.compat import urlencode
 from geopy.exc import GeocoderQueryError, GeocoderQuotaExceeded
@@ -28,6 +29,7 @@ class OpenCage(Geocoder):
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a customized OpenCageData geocoder.
@@ -58,6 +60,12 @@ class OpenCage(Geocoder):
 
             .. versionadded:: 1.14.0
 
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
+
         """
         super(OpenCage, self).__init__(
             format_string=format_string,
@@ -65,6 +73,7 @@ class OpenCage(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
 
         self.api_key = api_key
@@ -137,7 +146,7 @@ class OpenCage(Geocoder):
             self,
             query,
             language=None,
-            exactly_one=False,
+            exactly_one=DEFAULT_SENTINEL,
             timeout=DEFAULT_SENTINEL,
     ):
         """
@@ -153,12 +162,26 @@ class OpenCage(Geocoder):
         :param bool exactly_one: Return one result or a list of results, if
             available.
 
+            .. versionchanged:: 1.14.0
+               Default value for ``exactly_one`` was ``False``, which differs
+               from the conventional default across geopy. Please always pass
+               this argument explicitly, otherwise you would get a warning.
+               In geopy 2.0 the default value will become ``True``.
+
         :param int timeout: Time, in seconds, to wait for the geocoding service
             to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
             exception. Set this only if you wish to override, on this call
             only, the value set during the geocoder's initialization.
 
         """
+        if exactly_one is DEFAULT_SENTINEL:
+            warnings.warn('%s.reverse: default value for `exactly_one` '
+                          'argument will become True in geopy 2.0. '
+                          'Specify `exactly_one=False` as the argument '
+                          'explicitly to get rid of this warning.' % type(self).__name__,
+                          DeprecationWarning)
+            exactly_one = False
+
         params = {
             'key': self.api_key,
             'q': self._coerce_point_to_string(query),
diff --git a/geopy/geocoders/openmapquest.py b/geopy/geocoders/openmapquest.py
index 734c31f..3fd7771 100644
--- a/geopy/geocoders/openmapquest.py
+++ b/geopy/geocoders/openmapquest.py
@@ -25,6 +25,7 @@ class OpenMapQuest(Geocoder): # pylint: disable=W0223
             timeout=DEFAULT_SENTINEL,
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize an Open MapQuest geocoder with location-specific
@@ -52,6 +53,12 @@ class OpenMapQuest(Geocoder): # pylint: disable=W0223
             See :attr:`geopy.geocoders.options.default_user_agent`.
 
             .. versionadded:: 1.12.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
         super(OpenMapQuest, self).__init__(
             format_string=format_string,
@@ -59,6 +66,7 @@ class OpenMapQuest(Geocoder): # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         if not api_key:
             raise ConfigurationError('OpenMapQuest requires an API key')
diff --git a/geopy/geocoders/osm.py b/geopy/geocoders/osm.py
index 407f015..eb38cf0 100644
--- a/geopy/geocoders/osm.py
+++ b/geopy/geocoders/osm.py
@@ -43,6 +43,7 @@ class Nominatim(Geocoder):
             domain='nominatim.openstreetmap.org',
             scheme=None,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
             # Make sure to synchronize the changes of this signature in the
             # inheriting classes (e.g. PickPoint).
     ):
@@ -76,6 +77,12 @@ class Nominatim(Geocoder):
 
             .. versionadded:: 1.12.0
 
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
+
         """
         super(Nominatim, self).__init__(
             format_string=format_string,
@@ -83,6 +90,7 @@ class Nominatim(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.country_bias = country_bias
         self.view_box = view_box
diff --git a/geopy/geocoders/photon.py b/geopy/geocoders/photon.py
index f30beee..e6c7970 100644
--- a/geopy/geocoders/photon.py
+++ b/geopy/geocoders/photon.py
@@ -25,6 +25,7 @@ class Photon(Geocoder):  # pylint: disable=W0223
             proxies=DEFAULT_SENTINEL,
             domain='photon.komoot.de',
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a Photon/Komoot geocoder which aims to let you "search as
@@ -50,6 +51,12 @@ class Photon(Geocoder):  # pylint: disable=W0223
             See :attr:`geopy.geocoders.options.default_user_agent`.
 
             .. versionadded:: 1.12.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
         super(Photon, self).__init__(
             format_string=format_string,
@@ -57,6 +64,7 @@ class Photon(Geocoder):  # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.domain = domain.strip('/')
         self.api = "%s://%s/api" % (self.scheme, self.domain)
diff --git a/geopy/geocoders/pickpoint.py b/geopy/geocoders/pickpoint.py
index 0da392f..b4b5823 100644
--- a/geopy/geocoders/pickpoint.py
+++ b/geopy/geocoders/pickpoint.py
@@ -29,6 +29,7 @@ class PickPoint(Nominatim):
             domain='api.pickpoint.io',
             scheme=None,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
 
@@ -57,6 +58,12 @@ class PickPoint(Nominatim):
 
         :param str user_agent:
             See :attr:`geopy.geocoders.options.default_user_agent`.
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
 
         super(PickPoint, self).__init__(
@@ -68,6 +75,7 @@ class PickPoint(Nominatim):
             domain=domain,
             scheme=scheme,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.api = "%s://%s/v1/forward" % (self.scheme, self.domain)
diff --git a/geopy/geocoders/smartystreets.py b/geopy/geocoders/smartystreets.py
index 56dea97..3a39530 100644
--- a/geopy/geocoders/smartystreets.py
+++ b/geopy/geocoders/smartystreets.py
@@ -29,6 +29,7 @@ class LiveAddress(Geocoder):  # pylint: disable=W0223
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a customized SmartyStreets LiveAddress geocoder.
@@ -67,6 +68,12 @@ class LiveAddress(Geocoder):  # pylint: disable=W0223
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(LiveAddress, self).__init__(
@@ -78,6 +85,7 @@ class LiveAddress(Geocoder):  # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         if self.scheme == "http":
             raise ConfigurationError("LiveAddress now requires `https`.")
diff --git a/geopy/geocoders/what3words.py b/geopy/geocoders/what3words.py
index e039df2..fc1c20a 100644
--- a/geopy/geocoders/what3words.py
+++ b/geopy/geocoders/what3words.py
@@ -32,6 +32,7 @@ class What3Words(Geocoder):
             timeout=DEFAULT_SENTINEL,
             proxies=DEFAULT_SENTINEL,
             user_agent=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Initialize a What3Words geocoder with 3-word or OneWord-address and
@@ -57,6 +58,12 @@ class What3Words(Geocoder):
             See :attr:`geopy.geocoders.options.default_user_agent`.
 
             .. versionadded:: 1.12.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
+            .. versionadded:: 1.14.0
         """
         super(What3Words, self).__init__(
             format_string=format_string,
@@ -64,6 +71,7 @@ class What3Words(Geocoder):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.api = (
diff --git a/geopy/geocoders/yandex.py b/geopy/geocoders/yandex.py
index cc7f642..d788094 100644
--- a/geopy/geocoders/yandex.py
+++ b/geopy/geocoders/yandex.py
@@ -1,6 +1,7 @@
 """
 :class:`Yandex` geocoder.
 """
+import warnings
 
 from geopy.compat import urlencode
 from geopy.exc import GeocoderParseError, GeocoderServiceError
@@ -26,6 +27,7 @@ class Yandex(Geocoder): # pylint: disable=W0223
             user_agent=None,
             scheme=None,
             format_string=None,
+            ssl_context=DEFAULT_SENTINEL,
     ):
         """
         Create a Yandex-based geocoder.
@@ -60,6 +62,12 @@ class Yandex(Geocoder): # pylint: disable=W0223
         :param str format_string:
             See :attr:`geopy.geocoders.options.default_format_string`.
 
+            .. versionadded:: 1.14.0
+
+        :type ssl_context: :class:`ssl.SSLContext`
+        :param ssl_context:
+            See :attr:`geopy.geocoders.options.default_ssl_context`.
+
             .. versionadded:: 1.14.0
         """
         super(Yandex, self).__init__(
@@ -68,6 +76,7 @@ class Yandex(Geocoder): # pylint: disable=W0223
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
         self.api_key = api_key
         self.lang = lang
@@ -107,8 +116,9 @@ class Yandex(Geocoder): # pylint: disable=W0223
     def reverse(
             self,
             query,
-            exactly_one=False,
+            exactly_one=DEFAULT_SENTINEL,
             timeout=DEFAULT_SENTINEL,
+            kind=None,
     ):
         """
         Given a point, find an address.
@@ -121,12 +131,30 @@ class Yandex(Geocoder): # pylint: disable=W0223
         :param bool exactly_one: Return one result or a list of results, if
             available.
 
+            .. versionchanged:: 1.14.0
+               Default value for ``exactly_one`` was ``False``, which differs
+               from the conventional default across geopy. Please always pass
+               this argument explicitly, otherwise you would get a warning.
+               In geopy 2.0 the default value will become ``True``.
+
         :param int timeout: Time, in seconds, to wait for the geocoding service
             to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
             exception. Set this only if you wish to override, on this call
             only, the value set during the geocoder's initialization.
 
+        :param str kind: Type of toponym. Allowed values: `house`, `street`, `metro`,
+            `district`, `locality`.
+
+            .. versionadded:: 1.14.0
         """
+        if exactly_one is DEFAULT_SENTINEL:
+            warnings.warn('%s.reverse: default value for `exactly_one` '
+                          'argument will become True in geopy 2.0. '
+                          'Specify `exactly_one=False` as the argument '
+                          'explicitly to get rid of this warning.' % type(self).__name__,
+                          DeprecationWarning)
+            exactly_one = False
+
         try:
             lat, lng = [
                 x.strip() for x in
@@ -142,6 +170,8 @@ class Yandex(Geocoder): # pylint: disable=W0223
             params['apikey'] = self.api_key
         if self.lang:
             params['lang'] = self.lang
+        if kind:
+            params['kind'] = kind
         url = "?".join((self.api, urlencode(params)))
         logger.debug("%s.reverse: %s", self.__class__.__name__, url)
         return self._parse_json(
diff --git a/geopy/point.py b/geopy/point.py
index ab32add..94b1b82 100644
--- a/geopy/point.py
+++ b/geopy/point.py
@@ -68,22 +68,18 @@ def _normalize_coordinates(latitude, longitude, altitude):
 
     is_all_finite = all(isfinite(x) for x in (latitude, longitude, altitude))
     if not is_all_finite:
-        warnings.warn('Point coordinates should be finite. NaN or inf has '
-                      'been passed as a coordinate.  This will cause a '
-                      'ValueError exception in the future versions of geopy.',
-                      UserWarning)
+        raise ValueError('Point coordinates must be finite. %r has been passed '
+                         'as coordinates.' % ((latitude, longitude, altitude),))
 
     if abs(latitude) > 90:
-        warnings.warn('Latitude has been normalized, because its '
-                      'absolute value was more than 90. This is probably '
-                      'not what was meant, because the normalized value '
-                      'is on a different pole. If you pass coordinates as '
-                      'positional args, please make sure that the order is '
-                      '(latitude, longitude) or (y, x) in Cartesian terms.  '
-                      'This will cause a ValueError exception in the future '
-                      'versions of geopy.',
+        warnings.warn('Latitude normalization has been prohibited in the newer '
+                      'versions of geopy, because the normalized value happened '
+                      'to be on a different pole, which is probably not what was '
+                      'meant. If you pass coordinates as positional args, '
+                      'please make sure that the order is '
+                      '(latitude, longitude) or (y, x) in Cartesian terms.',
                       UserWarning)
-        latitude = _normalize_angle(latitude, 90.0)
+        raise ValueError('Latitude must be in the [-90; 90] range.')
 
     if abs(longitude) > 180:
         # Longitude normalization is pretty straightforward and doesn't seem


Test patch:
diff --git a/test/geocoders/base.py b/test/geocoders/base.py
index 1d586aa..4bf0371 100644
--- a/test/geocoders/base.py
+++ b/test/geocoders/base.py
@@ -1,8 +1,9 @@
 
 import unittest
-from mock import patch
+from mock import patch, sentinel
 import warnings
 
+import geopy.compat
 import geopy.geocoders
 from geopy.exc import GeocoderNotFound
 from geopy.geocoders import GoogleV3, get_geocoder_for_service
@@ -42,6 +43,7 @@ class GeocoderTestCase(unittest.TestCase):
         timeout = 942
         proxies = {'https': '192.0.2.0'}
         user_agent = 'test app'
+        ssl_context = sentinel.some_ssl_context
 
         geocoder = Geocoder(
             format_string=format_string,
@@ -49,8 +51,10 @@ class GeocoderTestCase(unittest.TestCase):
             timeout=timeout,
             proxies=proxies,
             user_agent=user_agent,
+            ssl_context=ssl_context,
         )
-        for attr in ('format_string', 'scheme', 'timeout', 'proxies'):
+        for attr in ('format_string', 'scheme', 'timeout', 'proxies',
+                     'ssl_context'):
             self.assertEqual(locals()[attr], getattr(geocoder, attr))
         self.assertEqual(user_agent, geocoder.headers['User-Agent'])
 
@@ -60,6 +64,7 @@ class GeocoderTestCase(unittest.TestCase):
             'scheme': 'default_scheme',
             'timeout': 'default_timeout',
             'proxies': 'default_proxies',
+            'ssl_context': 'default_ssl_context',
         }
 
         geocoder = Geocoder()
@@ -71,10 +76,13 @@ class GeocoderTestCase(unittest.TestCase):
 
     @patch.object(geopy.geocoders.options, 'default_proxies', {'https': '192.0.2.0'})
     @patch.object(geopy.geocoders.options, 'default_timeout', 10)
+    @patch.object(geopy.geocoders.options, 'default_ssl_context',
+                  sentinel.some_ssl_context)
     def test_init_with_none_overrides_default(self):
-        geocoder = Geocoder(proxies=None, timeout=None)
+        geocoder = Geocoder(proxies=None, timeout=None, ssl_context=None)
         self.assertIsNone(geocoder.proxies)
         self.assertIsNone(geocoder.timeout)
+        self.assertIsNone(geocoder.ssl_context)
 
     @patch.object(geopy.geocoders.options, 'default_user_agent', 'mocked_user_agent/0.0.0')
     def test_user_agent_default(self):
@@ -95,7 +103,9 @@ class GeocoderTestCase(unittest.TestCase):
         g = Geocoder()
         self.assertEqual(g.timeout, 12)
 
-        with patch.object(g, 'urlopen') as mock_urlopen:
+        # Suppress another (unrelated) warning when running tests on an old Python.
+        with patch('geopy.compat._URLLIB_SUPPORTS_SSL_CONTEXT', True), \
+                patch.object(g, 'urlopen') as mock_urlopen:
             g._call_geocoder(url, raw=True)
             args, kwargs = mock_urlopen.call_args
             self.assertEqual(kwargs['timeout'], 12)
@@ -111,6 +121,68 @@ class GeocoderTestCase(unittest.TestCase):
                 self.assertEqual(kwargs['timeout'], 12)
                 self.assertEqual(1, len(w))
 
+    def test_ssl_context_for_old_python(self):
+        # Before (exclusive) 2.7.9 and 3.4.3.
+
+        # Keep the reference, because `geopy.compat.HTTPSHandler` will be
+        # mocked below.
+        orig_HTTPSHandler = geopy.compat.HTTPSHandler
+
+        class HTTPSHandlerStub(geopy.compat.HTTPSHandler):
+            def __init__(self):  # No `context` arg.
+                orig_HTTPSHandler.__init__(self)
+
+        if hasattr(geopy.compat, '__warningregistry__'):
+            # If running tests on an old Python, the warning we are going
+            # to test might have been already issued and recorded in
+            # the registry. Clean it up, so we could receive the warning again.
+            del geopy.compat.__warningregistry__
+
+        with patch('geopy.compat._URLLIB_SUPPORTS_SSL_CONTEXT',
+                   geopy.compat._is_urllib_context_supported(HTTPSHandlerStub)), \
+                patch('geopy.compat.HTTPSHandler', HTTPSHandlerStub), \
+                warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            self.assertFalse(geopy.compat._URLLIB_SUPPORTS_SSL_CONTEXT)
+
+            self.assertEqual(0, len(w))
+            Geocoder()
+            self.assertEqual(1, len(w))
+
+    def test_ssl_context_for_newer_python(self):
+        # From (inclusive) 2.7.9 and 3.4.3.
+
+        # Keep the reference, because `geopy.compat.HTTPSHandler` will be
+        # mocked below.
+        orig_HTTPSHandler = geopy.compat.HTTPSHandler
+
+        class HTTPSHandlerStub(geopy.compat.HTTPSHandler):
+            def __init__(self, context=None):
+                orig_HTTPSHandler.__init__(self)
+
+        if hasattr(geopy.compat, '__warningregistry__'):
+            # If running tests on an old Python, the warning we are going
+            # to test might have been already issued and recorded in
+            # the registry. Clean it up, so we could receive the warning again.
+            del geopy.compat.__warningregistry__
+
+        with patch('geopy.compat._URLLIB_SUPPORTS_SSL_CONTEXT',
+                   geopy.compat._is_urllib_context_supported(HTTPSHandlerStub)), \
+                patch('geopy.compat.HTTPSHandler', HTTPSHandlerStub), \
+                patch.object(HTTPSHandlerStub, '__init__', autospec=True,
+                             side_effect=HTTPSHandlerStub.__init__
+                             ) as mock_https_handler_init, \
+                warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            self.assertTrue(geopy.compat._URLLIB_SUPPORTS_SSL_CONTEXT)
+
+            for ssl_context in (None, sentinel.some_ssl_context):
+                mock_https_handler_init.reset_mock()
+                Geocoder(ssl_context=ssl_context)
+                args, kwargs = mock_https_handler_init.call_args
+                self.assertIs(kwargs['context'], ssl_context)
+            self.assertEqual(0, len(w))
+
     def test_point_coercion_point(self):
         """
         Geocoder._coerce_point_to_string Point
diff --git a/test/geocoders/geonames.py b/test/geocoders/geonames.py
index f2feacd..22af7b8 100644
--- a/test/geocoders/geonames.py
+++ b/test/geocoders/geonames.py
@@ -44,6 +44,6 @@ class GeoNamesTestCase(GeocoderTestBase):
         # work around ConfigurationError raised in GeoNames init
         self.geocoder = GeoNames(username=env['GEONAMES_USERNAME'])
         self.reverse_run(
-            {"query": "40.75376406311989, -73.98489005863667"},
+            {"query": "40.75376406311989, -73.98489005863667", "exactly_one": True},
             {"latitude": 40.75376406311989, "longitude": -73.98489005863667},
         )
diff --git a/test/geocoders/googlev3.py b/test/geocoders/googlev3.py
index 1f24b56..f161b72 100644
--- a/test/geocoders/googlev3.py
+++ b/test/geocoders/googlev3.py
@@ -131,7 +131,7 @@ class GoogleV3TestCase(GeocoderTestBase): # pylint: disable=R0904,C0111
             {"latitude": 39.916, "longitude": 116.390},
         )
 
-    def test_geocode_components(self):
+    def test_geocode_with_conflicting_components(self):
         """
         GoogleV3.geocode returns None on conflicting components
         """
@@ -161,12 +161,20 @@ class GoogleV3TestCase(GeocoderTestBase): # pylint: disable=R0904,C0111
             {"latitude": 28.4636296, "longitude": -16.2518467},
         )
 
+    def test_components_without_query(self):
+        self.geocode_run(
+            {
+                "components": {"city": "Paris", "country": "FR"},
+            },
+            {"latitude": 46.227638, "longitude": 2.213749},
+        )
+
     def test_reverse_string(self):
         """
         GoogleV3.reverse string
         """
         self.reverse_run(
-            {"query": "40.75376406311989, -73.98489005863667"},
+            {"query": "40.75376406311989, -73.98489005863667", "exactly_one": True},
             {"latitude": 40.75376406311989, "longitude": -73.98489005863667},
         )
 
@@ -175,7 +183,7 @@ class GoogleV3TestCase(GeocoderTestBase): # pylint: disable=R0904,C0111
         GoogleV3.reverse Point
         """
         self.reverse_run(
-            {"query": self.new_york_point},
+            {"query": self.new_york_point, "exactly_one": True},
             {"latitude": 40.75376406311989, "longitude": -73.98489005863667},
         )
 
diff --git a/test/geocoders/ignfrance.py b/test/geocoders/ignfrance.py
index 003f47e..077e199 100644
--- a/test/geocoders/ignfrance.py
+++ b/test/geocoders/ignfrance.py
@@ -253,6 +253,7 @@ class IGNFranceTestCase(GeocoderTestBase):
         res = self._make_request(
             self.geocoder.reverse,
             query='47.229554,-1.541519',
+            exactly_one=False,
             reverse_geocode_preference=['StreetAddress', 'PositionOfInterest']
         )
         self.assertEqual(
@@ -278,12 +279,14 @@ class IGNFranceTestCase(GeocoderTestBase):
         res_call_radius = self._make_request(
             self.geocoder.reverse,
             query='48.8033333,2.3241667',
+            exactly_one=False,
             maximum_responses=10,
             filtering=spatial_filtering_radius)
 
         res_call = self._make_request(
             self.geocoder.reverse,
             query='48.8033333,2.3241667',
+            exactly_one=False,
             maximum_responses=10
         )
 
diff --git a/test/geocoders/util.py b/test/geocoders/util.py
index 66a3531..81a1f89 100644
--- a/test/geocoders/util.py
+++ b/test/geocoders/util.py
@@ -56,7 +56,8 @@ class GeocoderTestBase(unittest.TestCase): # pylint: disable=R0904
                 self.fail('No result found')
             else:
                 return
-        self._verify_request(result, **expected)
+        self._verify_request(result, exactly_one=payload.get('exactly_one', True),
+                             **expected)
         return result
 
     def reverse_run(self, payload, expected, expect_failure=False):
@@ -69,7 +70,8 @@ class GeocoderTestBase(unittest.TestCase): # pylint: disable=R0904
                 self.fail('No result found')
             else:
                 return
-        self._verify_request(result, **expected)
+        self._verify_request(result, exactly_one=payload.get('exactly_one', True),
+                             **expected)
         return result
 
     @staticmethod
@@ -94,11 +96,12 @@ class GeocoderTestBase(unittest.TestCase): # pylint: disable=R0904
             latitude=EMPTY,
             longitude=EMPTY,
             address=EMPTY,
-        ):
+            exactly_one=True,
+    ):
         """
         Verifies that a a result matches the kwargs given.
         """
-        item = result[0] if isinstance(result, (tuple, list)) else result
+        item = result if exactly_one else result[0]
 
         if raw is not EMPTY:
             self.assertEqual(item.raw, raw)
diff --git a/test/geocoders/yandex.py b/test/geocoders/yandex.py
index e7ead4b..5764fd3 100644
--- a/test/geocoders/yandex.py
+++ b/test/geocoders/yandex.py
@@ -64,7 +64,7 @@ class YandexTestCase(GeocoderTestBase):
         """
         self.geocoder = Yandex()
         self.reverse_run(
-            {"query": "40.75376406311989, -73.98489005863667"},
+            {"query": "40.75376406311989, -73.98489005863667", "exactly_one": True},
             {"latitude": 40.75376406311989, "longitude": -73.98489005863667},
         )
 
@@ -79,3 +79,10 @@ class YandexTestCase(GeocoderTestBase):
             {"query": "площа Леніна Донецьк"},
             {"address": "площа Леніна, Донецьк, Україна", "latitude": 48.002104, "longitude": 37.805186},
         )
+
+    def test_reverse_kind_param(self):
+        self.geocoder = Yandex()
+        self.reverse_run(
+            {"query": (55.743659, 37.408055), "kind": "locality", "exactly_one": True},
+            {"address": "Москва, Россия"}
+        )
diff --git a/test/selfsigned_ca.pem b/test/selfsigned_ca.pem
new file mode 100644
index 0000000..e9aceb8
--- /dev/null
+++ b/test/selfsigned_ca.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICyDCCAbACCQC8JTfjHqqtszANBgkqhkiG9w0BAQsFADAlMQ4wDAYDVQQKDAVn
+ZW9weTETMBEGA1UECwwKdW5pdCB0ZXN0czAgFw0xODA1MDkwODM5MDFaGA8yMTE4
+MDQxNTA4MzkwMVowJTEOMAwGA1UECgwFZ2VvcHkxEzARBgNVBAsMCnVuaXQgdGVz
+dHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtSG8S9I3M2C+eFcON
+fOafU4zCd9lQH4W3nY8L9GvvFBLNUXinWRCs4Oh3qaz2X3zlhOfFOXqFJQl653aN
+GfPUwWIRVa2q0jmuF7AJStjHx1aymnO28QJPK3sRzA2bIFD/v9iyLn4fGk6RkYnV
+Kf10uKpBDVuKvfTTPkujrYcqKelS03i0+ciC196LCPZdnWCEwjI/IiFtMfDIm2O6
+jOGTyKyMef74xl7sHbSrICwxkPM4e+DGbdkTpSKbWRz0QLoA+ygDuEtp0I+MAMr6
+1gaM7tttPdHluLjP7mB/vjn5qghs83qoVO29jap6gC/HK1bPqrerD5tOhArARtJF
+9F+XAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF/Bvn6l9GetXh6XZC+zgM4MC/kk
+SADd/nvo31TmkJgsBFa28CbGPDTfC/jy3ni3IW2sYfTaV9QDqfs5qQVADYqWVTIT
+Qz5x05UK5wxyWqlbd9wfGcK75ebYFqEaXXwvtuGKyHGwiTeAyUjSAvuwIdjnOe6H
+uYmte7LvzCzeuElm7hNTCZjI34sna3byX/Umq4Z2IlU6+IVE4M5kpEo1zyR6a3bw
+nQQkGgulYc5hqw1mIUtXTvafrwYbdk6GdOYzQh1gN6IRdeWVhdKrjrQj5rBgpN47
+4LFJhRyczmapflWX4MsROUJBN/UdsMvmfHMalS/rnFqo6YvQlDqBN0dIkCs=
+-----END CERTIFICATE-----
diff --git a/test/test_distance.py b/test/test_distance.py
index 22035e2..339c310 100644
--- a/test/test_distance.py
+++ b/test/test_distance.py
@@ -78,19 +78,14 @@ class CommonDistanceComputationCases(object):
         self.assertAlmostEqual(point.latitude, 0.0)
         self.assertAlmostEqual(point.longitude, 179.91, 3)
 
-    def test_should_tolerate_nans(self):
+    def test_should_not_tolerate_nans(self):
         nan = float('nan')
-        # This is probably a bad design to silently turn NaNs into NaNs
-        # instead of raising a ValueError, but this is the current behaviour,
-        # hence this test.
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter('always')
-            self.assertTrue(math.isnan(self.cls((nan, nan), (1, 1)).kilometers))
-            self.assertEqual(1, len(w))
-            self.assertTrue(math.isnan(self.cls((nan, 1), (1, nan)).kilometers))
-            self.assertEqual(3, len(w))  # 1 per each point tuple
-            self.assertTrue(math.isnan(self.cls((nan, 1), (nan, 1)).kilometers))
-            self.assertEqual(5, len(w))
+        with self.assertRaises(ValueError):
+            self.cls((nan, nan), (1, 1)).kilometers
+        with self.assertRaises(ValueError):
+            self.cls((nan, 1), (1, nan)).kilometers
+        with self.assertRaises(ValueError):
+            self.cls((nan, 1), (nan, 1)).kilometers
 
     def test_should_compute_distance_for_multiple_points_pairwise(self):
         dist_total = self.cls((10, 20), (40, 60), (0, 80), (0, 10))
@@ -125,8 +120,9 @@ class CommonDistanceComputationCases(object):
         with warnings.catch_warnings(record=True) as w:
             warnings.simplefilter('always')
             # The correct order is (lat, lon).
-            self.cls((lon, lat), (lon - 10, lat))
-            self.assertEqual(2, len(w))  # 1 per each point tuple
+            with self.assertRaises(ValueError):
+                self.cls((lon, lat), (lon - 10, lat))
+            self.assertEqual(1, len(w))
 
 
 class CommonMathematicalOperatorCases(object):
diff --git a/test/test_point.py b/test/test_point.py
index 0b5005c..34e364f 100644
--- a/test/test_point.py
+++ b/test/test_point.py
@@ -124,14 +124,22 @@ class PointTestCase(unittest.TestCase):
     def test_point_degrees_are_normalized(self):
         with warnings.catch_warnings(record=True) as w:
             warnings.simplefilter('always')
-            point = Point(95, 185, 375)
-            self.assertEqual((-85, -175, 375), tuple(point))
+            with self.assertRaises(ValueError):
+                # Latitude normalization is not allowed
+                Point(95, 185, 375)
             self.assertEqual(1, len(w))
 
-            point = Point(-95, -185, 375)
-            self.assertEqual((85, 175, 375), tuple(point))
+            with self.assertRaises(ValueError):
+                # Latitude normalization is not allowed
+                Point(-95, -185, 375)
             self.assertEqual(2, len(w))
 
+            point = Point(-85, 185, 375)
+            self.assertEqual((-85, -175, 375), tuple(point))
+
+            point = Point(85, -185, 375)
+            self.assertEqual((85, 175, 375), tuple(point))
+
             point = Point(-0.0, -0.0, 375)
             self.assertEqual((0.0, 0.0, 375.0), tuple(point))  # note that the zeros might be negative
             # ensure that negative zeros are normalized to the positive ones
@@ -142,13 +150,12 @@ class PointTestCase(unittest.TestCase):
             self.assertEqual((-90, -180, 375), tuple(point))
             self.assertEqual(2, len(w))
 
-            point = Point(-270, -540, 375)
+            point = Point(-90, -540, 375)
             self.assertEqual((-90, -180, 375), tuple(point))
-            self.assertEqual(3, len(w))
 
-            point = Point(270, 540, 375)
-            self.assertEqual((-90, -180, 375), tuple(point))
-            self.assertEqual(4, len(w))
+            point = Point(90, 540, 375)
+            self.assertEqual((90, -180, 375), tuple(point))
+            self.assertEqual(2, len(w))
 
     def test_point_degrees_normalization_does_not_lose_precision(self):
         if sys.float_info.mant_dig != 53:
@@ -173,13 +180,10 @@ class PointTestCase(unittest.TestCase):
         # (180.00000000000003 + 180) fraction doesn't fit in 52 bits.
         #
         # This test ensures that such unwanted precision loss is not happening.
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter('always')
-            self.assertEqual(tuple(Point(90.00000000000002, 180.00000000000003)),
-                             (-89.99999999999998, -179.99999999999997, 0))
-            self.assertEqual(tuple(Point(9.000000000000002, 1.8000000000000003)),
-                             (9.000000000000002, 1.8000000000000003, 0))
-            self.assertEqual(1, len(w))
+        self.assertEqual(tuple(Point(-89.99999999999998, 180.00000000000003)),
+                         (-89.99999999999998, -179.99999999999997, 0))
+        self.assertEqual(tuple(Point(9.000000000000002, 1.8000000000000003)),
+                         (9.000000000000002, 1.8000000000000003, 0))
 
     def test_unpacking(self):
         point = Point(self.lat, self.lon, self.alt)
@@ -241,14 +245,19 @@ class PointTestCase(unittest.TestCase):
         point = Point()
         with warnings.catch_warnings(record=True) as w:
             warnings.simplefilter('always')
-            point[0] = 100
+            with self.assertRaises(ValueError):
+                point[0] = 100
+            self.assertEqual(1, len(w))
+            self.assertEqual((0, 0, 0), tuple(point))
+            point[0] = -80
             point[1] = 200
             # Please note that attribute assignments are not normalized.
             # Only __setitem__ assignments are.
             self.assertEqual((-80, -160, 0), tuple(point))
-            self.assertEqual(1, len(w))  # for latitude
-            point[1] = float("nan")
-            self.assertEqual(2, len(w))
+            with self.assertRaises(ValueError):
+                point[1] = float("nan")
+            self.assertEqual(1, len(w))
+            self.assertEqual((-80, -160, 0), tuple(point))
 
     def test_point_assign_coordinates(self):
         point = Point(self.lat + 10, self.lon + 10, self.alt + 10)
diff --git a/test/test_proxy.py b/test/test_proxy.py
index a93e23b..5756223 100644
--- a/test/test_proxy.py
+++ b/test/test_proxy.py
@@ -1,16 +1,21 @@
 """
 Test ability to proxy requests.
 """
+import os
+import ssl
 import unittest
 from test.proxy_server import ProxyServerThread
 
 from geopy.compat import urlopen
+from geopy.exc import GeocoderServiceError
 from geopy.geocoders.base import Geocoder
 
+CERT_SELFSIGNED_CA = os.path.join(os.path.dirname(__file__), 'selfsigned_ca.pem')
+
 
 class DummyGeocoder(Geocoder):
     def geocode(self, location):
-        geo_request = self.urlopen(location, timeout=self.timeout)
+        geo_request = self._call_geocoder(location, raw=True)
         geo_html = geo_request.read()
         return geo_html if geo_html else None
 
@@ -54,3 +59,20 @@ class ProxyTestCase(unittest.TestCase): # pylint: disable=R0904,C0111
             geocoder_dummy.geocode(self.remote_website_https)
         )
         self.assertEqual(1, len(self.proxy_server.requests))
+
+    @unittest.skipUnless(
+        hasattr(ssl, 'create_default_context'),
+        "The current Python version doesn't support `ssl.create_default_context`."
+    )
+    def test_ssl_context_with_proxy_is_respected(self):
+        # Create an ssl context which should not allow the negotiation with
+        # the `self.remote_website_https`.
+        bad_ctx = ssl.create_default_context(cafile=CERT_SELFSIGNED_CA)
+        geocoder_dummy = DummyGeocoder(proxies={"https": self.proxy_url},
+                                       ssl_context=bad_ctx,
+                                       timeout=self.timeout)
+        self.assertEqual(0, len(self.proxy_server.requests))
+        with self.assertRaises(GeocoderServiceError) as cm:
+            geocoder_dummy.geocode(self.remote_website_https)
+        self.assertIn('SSL', str(cm.exception))
+        self.assertEqual(1, len(self.proxy_server.requests))