Repository: geopy/geopy
Pull request #240 (2017-04-11 15:11:56)

Add check for a NO_RESULT to GeocodeFarm
Issue  #239

Added two lines to check for the `NO_RESULTS` status from webservice and return `None` instead of an exception.

pembo13: @KostyaEsmukov test added in 6d8d023b4a847cdf6c0352d009e4141b28da7be9

Patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..ac9512f
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,10 @@
+[run]
+source = geopy
+
+[report]
+show_missing = True
+; Remember that tests run from forks and locally don't have access to
+; geocoders' credentials, that means that a significant amount of the code
+; (which requires these creds) cannot be tested. So coverage in CI differs
+; significantly between the builds for forks and for master.
+fail_under = 50
diff --git a/.gitignore b/.gitignore
index feb7c22..3559726 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ docs/_build
 .DS_Store
 .idea/
 .python-version
+.pytest_cache
diff --git a/.travis.yml b/.travis.yml
index 2878f9e..cf6632d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,7 +12,7 @@ matrix:
     allow_failures:
         - python: "3.7-dev"
 install:
-    - travis_retry pip install coverage nose-cov mock
-    - travis_retry pip install -e . ".[timezone,placefinder]"
+    - travis_retry pip install -e ".[dev,placefinder,timezone]"
 script:
-    - nosetests --verbose --with-cover --cover-erase --cover-package=geopy
+    - coverage run -m py.test
+    - coverage report
diff --git a/AUTHORS b/AUTHORS
index 4185b7e..921b334 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -12,6 +12,7 @@ Benjamin Henne <henne@dcsec.uni-hannover.de>
 Benjamin Trigona-Harany <bosth@alumni.sfu.ca>
 Benjamin Trigona-Harany <bosth@boundlessgeo.com>
 Benoit Grégoire <benoitg@coeus.ca>
+Bernd Schlapsi
 Brian Beck
 Charles Karney <charles@karney.com>
 crccheck <c@crccheck.com>
@@ -32,6 +33,7 @@ Feanil Patel <feanil@gmail.com>
 gary.bernhardt
 Gregory Nicholas <nicholas.g.gregory@gmail.com>
 groovecoder <luke.crouch@gmail.com>
+Hanno Schlichting <hanno@hannosch.eu>
 Ian Edwards
 Ian Wilson <ian.owings@gmail.com>
 ijl <uijllji@gmail.com>
@@ -59,6 +61,7 @@ michal
 Michal Migurski <mike@teczno.com>
 Mike Tigas <mike@tig.as>
 Mike Toews <mwtoews@gmail.com>
+mtmail <mtmail@gmx.net>
 mz <mattgeoc@autistici.org>
 navidata <kontakt@navidata.pl>
 nucflash <tsagias@gmail.com>
@@ -71,10 +74,14 @@ Philip Kimmey <philip.kimmey@gmail.com>
 Risent Zhang <shengqi542@gmail.com>
 Rocky Meza <rocky@fusionbox.com>
 Ryan Nagle <rnagle@tribune.com>
+scottessner <scott.essner@gmail.com>
 Sebastian Neubauer <sebineubauer@gmail.com>
 SemiNormal <bbengels@gmail.com>
+Serphentas
+svalee <triksrimer@gmail.com>
 Svetlana Konovalova
 Thomas <thomas_gratier@yahoo.fr>
 Tom Wallroth <tomwallroth@gmail.com>
 tristan <tristanhomsi@gmail.com>
+Vladimir Kalinkin <vova.kalinkin@gmail.com>
 willr <willr@plotwatt.com>
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d098f56..490ece9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,22 +1,101 @@
+# Contributing to geopy
+
+## Reporting issues
+
+- Please note that the issue tracker is not for questions. Use Stack Overflow
+  instead. Make sure to tag your question with
+  [geopy](https://stackoverflow.com/questions/tagged/geopy) tag.
+
+- If possible, before submitting an issue report try to verify that the issue
+  haven't already been fixed and is not a duplicate.
+
+## Submitting patches
+
 If you contribute code to geopy, you agree to license your code under the MIT.
 
 geopy runs on both Python 2 and Python 3 on both the CPython and PyPy
 interpreters. You should handle any compatibility in `geopy/compat.py`.
 
+The new code should follow [PEP8](https://pep8.org/) coding style (except
+the line length limit) and adhere to the style of the surrounding code.
+
 You must document any functionality using Sphinx-compatible RST, and
 implement tests for any functionality in the `test` directory.
 
+In your Pull Requests there's no need to fill the changelog or AUTHORS,
+this is a maintainer's responsibility.
+
+For your convenience the contributing-friendly issues are marked with
+`help wanted` label, and the beginner-friendly ones with
+`good first issue`.
+
+If your PR remains unreviewed for a while, feel free to bug the maintainer.
+
+### Setup
+
+1. Create a virtualenv
+2. Install `geopy` in editable mode along with dev dependencies:
+
+        pip install -e ".[dev]"
+
+3. Ensure that the tests pass
+
+        pytest
+
+### Running tests
+
+To run the full test suite:
+
+    pytest
+
+To run a specific test, pass a path as an argument. For example:
+
+    pytest test/geocoders/nominatim.py
+
+### Geocoder credentials
+
+Some geocoders require credentials (like API keys) for testing. They must
+remain secret, so if you need to test a code which requires them, you should
+obtain your own valid credentials.
+
+Travis CI builds for PRs from forks don't have access to these
+credentials to avoid the possibility of leaking the secrets by running
+untrusted code. This is a [Travis CI policy][travis_env]. It means that CI
+builds for PRs from forks won't test the code which require credentials.
+Such code should be tested locally. But it's acceptable to not test such code
+if obtaining the required credentials seems problematic: just leave a note
+so the maintainer would be aware that the code hasn't been tested.
+
+[travis_env]: https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings
+
+You may wonder: why not commit captured data and run mocked tests?
+Because there are copyright constraints on the data returned by services.
+
+The credentials can be stored in a json format in a file called `.test_keys`
+located at the root of the project instead of env variables for convenience.
+
+Example contents of `.test_keys`:
+
+    {
+        "BING_KEY": "...secret key...",
+        "OPENCAGE_KEY": "...secret key..."
+    }
+
+### Building docs
+
+    cd docs
+    make html
+
+Open `_build/html/index.html` with a browser to see the docs. On macOS you 
+can use the following command for that:
+
+    open _build/html/index.html
+
+### Improving a geocoder
+
 If you want to add additional parameters to a `geocode` or `reverse`
 method, the additional parameters must be explicitly specified and documented
 in the method signature. Validation may be done for type, but values should
 probably not be checked against an enumerated list because the service could
 change.
 
-Full integration tests do not run on geopy's Travis CI build because the build
-depends on credentials that must remain secret, and a branch could echo
-those secrets. To test your changes, obtain valid credentials for the
-service or services affected, and run tests on that section locally. The
-maintainer will run tests and may alert you to failures in services you have
-not tested. You may wonder: why not commit captured data and run mocked
-tests? Because there are copyright constraints on the data returned by
-services.
diff --git a/Makefile b/Makefile
index 75ec27a..495d6bb 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,8 @@ pylint:
 	pylint --rcfile .pylintrc geopy
 
 test:
-	nosetests --verbose --with-cover --cover-erase --cover-package=geopy
+	coverage run -m py.test
+	coverage report
 
 clean:
 	find . -name "*.pyc" -print0 | xargs -0 rm -rf
diff --git a/docs/changelog_1xx.rst b/docs/changelog_1xx.rst
index 12f0e56..dc0bfcf 100644
--- a/docs/changelog_1xx.rst
+++ b/docs/changelog_1xx.rst
@@ -3,6 +3,65 @@
 Changelog
 =========
 
+1.13.0
+------
+2018-04-12
+
+*   ADDED: Pickpoint geocoder. Contributed by Vladimir Kalinkin. (#246)
+
+*   ADDED: Bing geocoder: additional parameters for geocoding (`culture`
+    and `include_country_code`). Contributed by Bernd Schlapsi. (#166)
+
+*   ADDED: `Point` and `Location` instances are now picklable.
+
+*   ADDED: More accurate algorithm for distance computation
+    `geopy.distance.geodesic`, which is now a default
+    `geopy.distance.distance`. Vincenty usage is now discouraged in favor of
+    the geodesic. This also has added a dependency of geopy on
+    `geographiclib` package. Contributed by Charles Karney. (#144)
+
+*   ADDED: Nominatim geocoder now supports a `limit` option and uses `limit=1`
+    for `exactly_one=True` requests. Contributed by Serphentas. (#281)
+
+*   CHANGED: `Point` now issues warnings for incorrect or ambiguous inputs.
+    Some of them (namely not finite values and out of band latitudes)
+    will be replaced with ValueError exceptions in the future versions
+    of geopy. (#272)
+
+*   CHANGED: `Point` now uses `fmod` instead of `%` which results in more
+    accurate coordinates normalization. Contributed by svalee. (#275, #279)
+
+*   CHANGED: When using http proxy, urllib's `install_opener` was used, which
+    was altering `urlopen` call globally. It's not used anymore.
+
+*   CHANGED: `Point` now raises `ValueError` instead of `TypeError` when more
+    than 3 arguments have been passed.
+
+*   FIXED: `Point` was raising an exception when compared to non-iterables.
+
+*   FIXED: Coordinates of a `Point` instance changed via `__setitem__` were
+    not updating the corresponding lat/long/alt attributes.
+
+*   FIXED: Coordinates of a `Point` instance changed via `__setitem__` were
+    not being normalized after assignment. Note, however, that attribute
+    assignments are still not normalized. (#272)
+
+*   FIXED: `Distance` instances comparison was not working in Python3.
+
+*   FIXED: Yandex geocoder was sending API key with an incorrect parameter.
+
+*   FIXED: Unit conversions from feet were incorrect.
+    Contributed by scottessner. (#162)
+
+*   FIXED: Vincenty destination function had an error in the formula
+    implementation. Contributed by Hanno Schlichting. (#194)
+
+*   FIXED: Vincenty was throwing UnboundLocalError when difference between
+    the two longitudes was close to 2*pi or either of them was NaN. (#187)
+
+*   REMOVED: `geopy.util.NullHandler` logging handler has been removed.
+
+
 1.12.0
 ------
 2018-03-13
diff --git a/geopy/compat.py b/geopy/compat.py
index 13957ad..3f310ee 100644
--- a/geopy/compat.py
+++ b/geopy/compat.py
@@ -36,6 +36,16 @@ if py3k:
 else:
     cmp = cmp  # builtin in py2
 
+
+if py3k:
+    from math import isfinite
+else:
+    from math import isinf, isnan
+
+    def isfinite(x):
+        return not isinf(x) and not isnan(x)
+
+
 if py3k: # pragma: no cover
     from urllib.parse import (urlencode, quote, # pylint: disable=W0611,F0401,W0611,E0611
                               urlparse, parse_qs)
diff --git a/geopy/distance.py b/geopy/distance.py
index 45b370f..d95eafb 100644
--- a/geopy/distance.py
+++ b/geopy/distance.py
@@ -341,6 +341,9 @@ class geodesic(Distance):
         >>> cleveland_oh = (41.499498, -81.695391)
         >>> print(geodesic(newport_ri, cleveland_oh).miles)
         538.390445368
+
+
+    .. versionadded:: 1.13.0
     """
 
     ellipsoid_key = None
@@ -418,6 +421,8 @@ GeodesicDistance = geodesic
 class vincenty(Distance):
     """
     .. deprecated:: 1.13
+       Use :class:`.geodesic` instead.
+
     Calculate the geodesic distance between two points using the Vincenty's
     method.
 
diff --git a/geopy/geocoders/geocodefarm.py b/geopy/geocoders/geocodefarm.py
index 72e3cef..40a6c9e 100644
--- a/geopy/geocoders/geocodefarm.py
+++ b/geopy/geocoders/geocodefarm.py
@@ -150,6 +150,8 @@ class GeocodeFarm(Geocoder):
         geocoding_results = api_result["geocoding_results"]
         self._check_for_api_errors(geocoding_results)
 
+        if "NO_RESULTS" in geocoding_results.get("STATUS", {}).get("status", ""): return None
+
         places = self.parse_code(geocoding_results)
         if exactly_one:
             return places[0]
@@ -163,6 +165,7 @@ class GeocodeFarm(Geocoder):
         in the api response.
         """
         status_result = geocoding_results.get("STATUS", {})
+        if "NO_RESULTS" in status_result.get("status", ""): return
         api_call_success = status_result.get("status", "") == "SUCCESS"
         if not api_call_success:
             access_error = status_result.get("access")
diff --git a/geopy/point.py b/geopy/point.py
index 4c14c59..ab32add 100644
--- a/geopy/point.py
+++ b/geopy/point.py
@@ -5,6 +5,7 @@
 
 import collections
 import re
+import warnings
 from math import fmod
 from itertools import islice
 from geopy import util, units
@@ -15,7 +16,7 @@ from geopy.format import (
     format_degrees,
     format_distance,
 )
-from geopy.compat import string_compare
+from geopy.compat import string_compare, isfinite
 
 
 POINT_PATTERN = re.compile(r"""
@@ -60,6 +61,38 @@ def _normalize_angle(x, limit):
     return modulo
 
 
+def _normalize_coordinates(latitude, longitude, altitude):
+    latitude = float(latitude or 0.0)
+    longitude = float(longitude or 0.0)
+    altitude = float(altitude or 0.0)
+
+    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)
+
+    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.',
+                      UserWarning)
+        latitude = _normalize_angle(latitude, 90.0)
+
+    if abs(longitude) > 180:
+        # Longitude normalization is pretty straightforward and doesn't seem
+        # to be error-prone, so there's nothing to complain about.
+        longitude = _normalize_angle(longitude, 180.0)
+
+    return latitude, longitude, altitude
+
+
 class Point(object):
     """
     A geodetic point with latitude, longitude, and altitude.
@@ -124,12 +157,10 @@ class Point(object):
         :param float longitude: Longitude of point.
         :param float altitude: Altitude of point.
         """
-        single_arg = longitude is None and altitude is None
+        single_arg = latitude is not None and longitude is None and altitude is None
         if single_arg and not isinstance(latitude, util.NUMBER_TYPES):
             arg = latitude
-            if arg is None: # pragma: no cover
-                pass
-            elif isinstance(arg, Point):
+            if isinstance(arg, Point):
                 return cls.from_point(arg)
             elif isinstance(arg, string_compare):
                 return cls.from_string(arg)
@@ -143,15 +174,17 @@ class Point(object):
                 else:
                     return cls.from_sequence(seq)
 
-        latitude = float(latitude or 0.0)
-        if abs(latitude) > 90:
-            latitude = _normalize_angle(latitude, 90.0)
-
-        longitude = float(longitude or 0.0)
-        if abs(longitude) > 180:
-            longitude = _normalize_angle(longitude, 180.0)
+        if single_arg:
+            warnings.warn('A single number has been passed to the Point '
+                          'constructor. This is probably a mistake, because '
+                          'constructing a Point with just a latitude '
+                          'seems senseless. If this is exactly what was '
+                          'meant, then pass the zero longitude explicitly '
+                          'to get rid of this warning.',
+                          UserWarning)
 
-        altitude = float(altitude or 0.0)
+        latitude, longitude, altitude = \
+            _normalize_coordinates(latitude, longitude, altitude)
 
         self = super(Point, cls).__new__(cls)
         self.latitude = latitude
@@ -165,7 +198,8 @@ class Point(object):
     def __setitem__(self, index, value):
         point = list(self)
         point[index] = value  # list handles slices
-        self.latitude, self.longitude, self.altitude = point
+        self.latitude, self.longitude, self.altitude = \
+            _normalize_coordinates(*point)
 
     def __iter__(self):
         return iter((self.latitude, self.longitude, self.altitude))
diff --git a/geopy/util.py b/geopy/util.py
index ed8f399..922986f 100644
--- a/geopy/util.py
+++ b/geopy/util.py
@@ -16,7 +16,7 @@ except ImportError: # pragma: no cover
     pass
 
 
-__version__ = "1.12.0"
+__version__ = "1.13.0"
 
 logger = logging.getLogger('geopy') # pylint: disable=C0103
 
diff --git a/setup.py b/setup.py
index c395171..38bc110 100755
--- a/setup.py
+++ b/setup.py
@@ -25,6 +25,12 @@ setup(
     packages=find_packages(exclude=["*test*"]),
     install_requires=INSTALL_REQUIRES,
     extras_require={
+        "dev": [
+            "coverage",
+            "pytest>=3",
+            "mock",
+            "sphinx",
+        ],
         "placefinder": ["requests_oauthlib<1,>=0.4.0"],
         "timezone": ["pytz"],
     },
diff --git a/tox.ini b/tox.ini
index 28a5419..31e4f59 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,13 +2,12 @@
 envlist=py{27,34,35,36,37},pypy{,3},lint
 
 [testenv]
-deps=
-    nose-cov
-    requests-oauthlib
-    pytz
-    mock
+extras =
+    dev
+    placefinder
+    timezone
 
-commands=nosetests --verbose
+commands = pytest
 
 [testenv:lint]
 deps = pylint


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..ff2a819
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,9 @@
+[pytest]
+python_files = test/test_*.py test/geocoders/*.py
+
+; Show warnings. Similar to `python -Wd`.
+filterwarnings = d
+
+; Show skip reasons
+; Print shorter tracebacks
+addopts = -ra --tb=short
diff --git a/test/__init__.py b/test/__init__.py
index 53188fe..e69de29 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,2 +0,0 @@
-
-from .geocoders import *
diff --git a/test/geocoders/__init__.py b/test/geocoders/__init__.py
index f53607b..e69de29 100644
--- a/test/geocoders/__init__.py
+++ b/test/geocoders/__init__.py
@@ -1,21 +0,0 @@
-
-from .arcgis import *
-from .baidu import *
-from .base import *
-from .bing import *
-from .databc import *
-from .dotus import *
-from .geocodefarm import *
-from .geonames import *
-from .googlev3 import *
-from .ignfrance import *
-from .mapzen import *
-from .nominatim import *
-from .opencage import *
-from .openmapquest import *
-from .pickpoint import *
-from .photon import *
-from .placefinder import *
-from .smartystreets import *
-from .what3words import *
-from .yandex import *
diff --git a/test/geocoders/baidu.py b/test/geocoders/baidu.py
index d10871a..b9d127b 100644
--- a/test/geocoders/baidu.py
+++ b/test/geocoders/baidu.py
@@ -43,18 +43,6 @@ class BaiduTestCase(GeocoderTestBase):
             {"latitude": 39.983615544507, "longitude": 116.32295155093},
         )
 
-    def test_reverse_address(self):
-        """
-        Baidu.reverse address
-        """
-        self.reverse_run(
-            {"query": u(
-                "\u5317\u4eac\u5e02\u6d77\u6dc0\u533a\u4e2d"
-                "\u5173\u6751\u5927\u885727\u53f7"
-            )},
-            {"latitude": 39.983615544507, "longitude": 116.32295155093},
-        )
-
     def test_reverse_point(self):
         """
         Baidu.reverse Point
diff --git a/test/geocoders/geocodefarm.py b/test/geocoders/geocodefarm.py
index 481937b..4adf9a5 100644
--- a/test/geocoders/geocodefarm.py
+++ b/test/geocoders/geocodefarm.py
@@ -96,6 +96,31 @@ class GeocodeFarmTestCase(GeocoderTestBase): # pylint: disable=R0904,C0111
         with self.assertRaises(exc.GeocoderQuotaExceeded):
             self.geocoder.geocode('435 north michigan ave, chicago il 60611')
 
+    def test_no_results(self):
+        def mock_call_geocoder(*args, **kwargs):
+            """
+            Mock API call to return bad response.
+            """
+            return {
+                "geocoding_results": {
+                    "STATUS": {
+                        "access": "KEY_VALID, ACCESS_GRANTED",
+                        "status": "FAILED, NO_RESULTS"
+                    }
+                }
+            }
+
+        self.geocoder._call_geocoder = types.MethodType(
+            mock_call_geocoder,
+            self.geocoder
+        )
+
+        self.geocode_run(
+            {"query": "435 north michigan ave, tampa fl 60611 usa"},
+            None,
+            expect_failure=True
+        )
+
     def test_unhandled_api_error(self):
         """
         GeocodeFarm unhandled error
diff --git a/test/test_distance.py b/test/test_distance.py
index f529f45..a2506a3 100644
--- a/test/test_distance.py
+++ b/test/test_distance.py
@@ -5,8 +5,7 @@ import math
 import unittest
 import warnings
 
-from nose.tools import assert_raises, assert_almost_equal # pylint: disable=E0611
-
+from geopy import distance as geopy_distance
 from geopy.point import Point
 from geopy.distance import (Distance,
                             GreatCircleDistance,
@@ -35,60 +34,62 @@ class CommonDistanceComputationCases(object):
 
     def test_should_have_length_when_only_given_length(self):
         distance = 1
-        assert self.cls(distance).kilometers == distance
+        self.assertEqual(self.cls(distance).kilometers, distance)
 
     def test_should_have_zero_distance_for_coincident_points(self):
-        assert self.cls((0, 0), (0, 0)).kilometers == 0
+        self.assertEqual(self.cls((0, 0), (0, 0)).kilometers, 0)
 
     def test_should_have_nonzero_distance_for_distinct_points(self):
-        assert self.cls((0, 0), (0, 1)).kilometers > 0
+        self.assertTrue(self.cls((0, 0), (0, 1)).kilometers > 0)
 
     def test_max_longitude(self):
         distance = self.cls(kilometers=1.0)
         destination = distance.destination(FIJI, 45)
-        assert_almost_equal(destination.longitude, -179.99338, 4)
+        self.assertAlmostEqual(destination.longitude, -179.99338, 4)
 
     def test_should_compute_distance_for_trip_between_poles(self):
         distance = self.cls(SOUTH_POLE, NORTH_POLE)
         expected_distance = EARTH_CIRCUMFERENCE / 2
-        assert_almost_equal(distance.kilometers, expected_distance, -2)
+        self.assertAlmostEqual(distance.kilometers, expected_distance, -2)
 
     def test_should_compute_destination_for_trip_between_poles(self):
         distance = self.cls(EARTH_CIRCUMFERENCE / 2)
         destination = distance.destination(NORTH_POLE, 0)
-        assert_almost_equal(destination.latitude, -90, 0)
-        assert_almost_equal(destination.longitude, 0)
+        self.assertAlmostEqual(destination.latitude, -90, 0)
+        self.assertAlmostEqual(destination.longitude, 0)
 
     def test_should_recognize_equivalence_of_pos_and_neg_180_longitude(self):
         distance1 = self.cls((0, -180), (0, 180)).kilometers
         distance2 = self.cls((0, 180), (0, -180)).kilometers
-        assert_almost_equal(distance1, 0)
-        assert_almost_equal(distance2, 0)
-
-        distance = self.cls((0, -180), (0, 180)).kilometers
-        assert_almost_equal(distance, 0)
+        self.assertAlmostEqual(distance1, 0)
+        self.assertAlmostEqual(distance2, 0)
 
     def test_should_compute_distance_across_antimeridian(self):
         nines = 1 - 1e-30  # 0.(9)
         distance = self.cls((0, -179 - nines),
                             (0, 179 + nines)).kilometers
-        assert_almost_equal(distance, 0)
+        self.assertAlmostEqual(distance, 0)
 
     def test_should_compute_destination_across_antimeridian(self):
         nines = 1 - 1e-30  # 0.(9)
         distance = self.cls(10)
         point = distance.destination((0, -179 - nines), -90)
-        assert_almost_equal(point.latitude, 0.0)
-        assert_almost_equal(point.longitude, 179.91, 3)
+        self.assertAlmostEqual(point.latitude, 0.0)
+        self.assertAlmostEqual(point.longitude, 179.91, 3)
 
     def test_should_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.
-        self.assertTrue(math.isnan(self.cls((nan, nan), (1, 1)).kilometers))
-        self.assertTrue(math.isnan(self.cls((nan, 1), (1, nan)).kilometers))
-        self.assertTrue(math.isnan(self.cls((nan, 1), (nan, 1)).kilometers))
+        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))
 
     def test_should_compute_distance_for_multiple_points_pairwise(self):
         dist_total = self.cls((10, 20), (40, 60), (0, 80), (0, 10))
@@ -96,8 +97,35 @@ class CommonDistanceComputationCases(object):
         dist2 = self.cls((40, 60), (0, 80))
         dist3 = self.cls((0, 80), (0, 10))
 
-        assert_almost_equal(dist_total.kilometers,
-                            dist1.km + dist2.km + dist3.km)
+        self.assertAlmostEqual(dist_total.kilometers,
+                               dist1.km + dist2.km + dist3.km)
+
+    def test_should_warn_when_using_single_numbers_as_points(self):
+        # Each argument is expected to be a Point. If it's not a point,
+        # it will be wrapped in Point.
+        # Point(10) equals to Point(10, 0).
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            dist1 = self.cls(10, 20)
+            self.assertEqual(2, len(w))  # 1 per each point
+            dist2 = self.cls((10, 0), (20, 0))
+            # no warnings: explicit tuples are not that suspicious
+            self.assertEqual(2, len(w))
+        self.assertAlmostEqual(dist1.kilometers, dist2.kilometers)
+
+    def test_should_tolerate_tuples_with_textual_numbers(self):
+        dist1 = self.cls(("1", "30"), ("20", "60"))
+        dist2 = self.cls((1, 30), (20, 60))
+        self.assertAlmostEqual(dist1.kilometers, dist2.kilometers)
+
+    def test_should_warn_for_mixed_up_lat_lon(self):
+        lat = 40
+        lon = 120  # should exceed max lat (abs 90) to cause a warning
+        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
 
 
 class CommonMathematicalOperatorCases(object):
@@ -106,51 +134,51 @@ class CommonMathematicalOperatorCases(object):
 
     def test_should_be_able_to_add_distances(self):
         added = self.cls(1.0) + self.cls(1.0)
-        assert_almost_equal(added.kilometers, 2.0)
+        self.assertAlmostEqual(added.kilometers, 2.0)
 
     def test_should_not_allow_adding_with_objects_that_arent_distances(self):
-        assert_raises(TypeError, lambda: self.cls(1.0) + 5)
+        with self.assertRaises(TypeError):
+            self.cls(1.0) + 5
 
     def test_should_be_able_to_negate_distances(self):
         distance = self.cls(1.0)
-        assert_almost_equal(-(distance.kilometers),
-                            (-distance).kilometers)
+        self.assertAlmostEqual(-(distance.kilometers),
+                               (-distance).kilometers)
 
     def test_should_be_able_to_subtract_distances(self):
         subtracted = self.cls(2.0) - self.cls(1.0)
-        assert_almost_equal(subtracted.kilometers, 1)
+        self.assertAlmostEqual(subtracted.kilometers, 1)
 
     def test_should_be_able_to_multiply_distances_by_floats(self):
-        assert_almost_equal((self.cls(2.0) * 2.0).kilometers,
-                            4.0)
+        self.assertAlmostEqual((self.cls(2.0) * 2.0).kilometers, 4.0)
 
     def test_should_not_be_able_to_multiply_distances_by_distances(self):
-        assert_raises(TypeError, lambda: self.cls(1.0) * self.cls(2.0))
+        with self.assertRaises(TypeError):
+            self.cls(1.0) * self.cls(2.0)
 
     def test_should_be_able_to_divide_distances_by_distances(self):
         ratio = self.cls(4.0) / self.cls(2.0)
         self.assertIsInstance(ratio, float)
-        assert_almost_equal(ratio, 2.0)
+        self.assertAlmostEqual(ratio, 2.0)
 
     def test_should_be_able_to_divide_distances_by_floats(self):
         divided_distance = self.cls(4.0) / 2.0
         self.assertIsInstance(divided_distance, self.cls)
-        assert_almost_equal(divided_distance.kilometers, 2.0)
+        self.assertAlmostEqual(divided_distance.kilometers, 2.0)
 
     def test_should_be_able_to_take_absolute_value_of_distances(self):
-        assert_almost_equal(abs(self.cls(-1.0)).kilometers,
-                            1.0)
+        self.assertAlmostEqual(abs(self.cls(-1.0)).kilometers, 1.0)
 
     def test_should_be_true_in_boolean_context_when_nonzero_length(self):
-        assert self.cls(1.0)
+        self.assertTrue(self.cls(1.0))
 
     def test_should_be_false_in_boolean_context_when_zero_length(self):
-        assert not self.cls(0)
+        self.assertFalse(self.cls(0))
 
     def test_should_get_consistent_results_for_distance_calculations(self):
         distance1, distance2 = [self.cls((0, 0), (0, 1))
                                 for _ in range(2)]
-        assert distance1.kilometers == distance2.kilometers
+        self.assertEqual(distance1.kilometers, distance2.kilometers)
 
 
 class CommonConversionCases(object):
@@ -158,46 +186,46 @@ class CommonConversionCases(object):
     cls = None
 
     def test_should_convert_to_kilometers(self):
-        assert self.cls(1.0).kilometers == 1.0
+        self.assertEqual(self.cls(1.0).kilometers, 1.0)
 
     def test_should_convert_to_kilometers_with_abbreviation(self):
-        assert self.cls(1.0).km == 1.0
+        self.assertEqual(self.cls(1.0).km, 1.0)
 
     def test_should_convert_to_meters(self):
-        assert self.cls(1.0).meters == 1000.0
+        self.assertEqual(self.cls(1.0).meters, 1000.0)
 
     def test_should_convert_to_meters_with_abbreviation(self):
-        assert self.cls(1.0).m == 1000.0
+        self.assertEqual(self.cls(1.0).m, 1000.0)
 
     def test_should_convert_to_miles(self):
-        assert_almost_equal(self.cls(1.0).miles, 0.621371192)
+        self.assertAlmostEqual(self.cls(1.0).miles, 0.621371192)
 
     def test_should_convert_to_miles_with_abbreviation(self):
-        assert_almost_equal(self.cls(1.0).mi, 0.621371192)
+        self.assertAlmostEqual(self.cls(1.0).mi, 0.621371192)
 
     def test_should_convert_to_feet(self):
-        assert_almost_equal(self.cls(1.0).feet, 3280.8399, 4)
+        self.assertAlmostEqual(self.cls(1.0).feet, 3280.8399, 4)
 
     def test_should_convert_to_feet_with_abbreviation(self):
-        assert_almost_equal(self.cls(1.0).ft, 3280.8399, 4)
+        self.assertAlmostEqual(self.cls(1.0).ft, 3280.8399, 4)
 
     def test_should_convert_to_nautical_miles(self):
-        assert_almost_equal(self.cls(1.0).nautical, 0.539956803)
+        self.assertAlmostEqual(self.cls(1.0).nautical, 0.539956803)
 
     def test_should_convert_to_nautical_miles_with_abbrevation(self):
-        assert_almost_equal(self.cls(1.0).nm, 0.539956803)
+        self.assertAlmostEqual(self.cls(1.0).nm, 0.539956803)
 
     def test_should_convert_from_meters(self):
-        assert(self.cls(meters=1.0).km == 0.001)
+        self.assertEqual(self.cls(meters=1.0).km, 0.001)
 
     def test_should_convert_from_feet(self):
-        assert_almost_equal(self.cls(feet=1.0).km, 0.0003048, 8)
+        self.assertAlmostEqual(self.cls(feet=1.0).km, 0.0003048, 8)
 
     def test_should_convert_from_miles(self):
-        assert_almost_equal(self.cls(miles=1.0).km, 1.6093440)
+        self.assertAlmostEqual(self.cls(miles=1.0).km, 1.6093440)
 
     def test_should_convert_from_nautical_miles(self):
-        assert_almost_equal(self.cls(nautical=1.0).km, 1.8520000)
+        self.assertAlmostEqual(self.cls(nautical=1.0).km, 1.8520000)
 
 
 class CommonComparisonCases(object):
@@ -244,7 +272,8 @@ class CommonDistanceCases(CommonDistanceComputationCases,
 
 class TestWhenInstantiatingBaseDistanceClass(unittest.TestCase):
     def test_should_not_be_able_to_give_multiple_points(self):
-        assert_raises(NotImplementedError, lambda: Distance(1, 2, 3, 4))
+        with self.assertRaises(NotImplementedError):
+            Distance(1, 2, 3, 4)
 
 
 class TestDefaultDistanceClass(unittest.TestCase):
@@ -267,13 +296,13 @@ class TestWhenComputingGreatCircleDistance(CommonDistanceCases,
 
     def test_should_compute_distance_for_half_trip_around_equator(self):
         distance_around_earth = self.cls((0, 0), (0, 180)).kilometers
-        assert distance_around_earth == EARTH_CIRCUMFERENCE / 2
+        self.assertEqual(distance_around_earth, EARTH_CIRCUMFERENCE / 2)
 
     def test_should_compute_destination_for_half_trip_around_equator(self):
         distance = self.cls(EARTH_CIRCUMFERENCE / 2)
         destination = distance.destination((0, 0), 0)
-        assert_almost_equal(destination.latitude, 0)
-        assert_almost_equal(destination.longitude, 180)
+        self.assertAlmostEqual(destination.latitude, 0)
+        self.assertAlmostEqual(destination.longitude, 180)
 
 
 class TestWhenComputingVincentyDistance(CommonDistanceCases,
@@ -288,36 +317,39 @@ class TestWhenComputingVincentyDistance(CommonDistanceCases,
         self.cls.ELLIPSOID = self.original_ellipsoid
 
     def test_should_not_converge_for_half_trip_around_equator(self):
-        assert_raises(ValueError, self.cls, (0, 0), (0, 180))
+        with self.assertRaises(ValueError):
+            self.cls((0, 0), (0, 180))
 
     def test_should_compute_destination_for_half_trip_around_equator(self):
         distance = self.cls()
         destination = distance.destination((0, 0), 90,
                                            math.pi * distance.ELLIPSOID[0])
-        assert_almost_equal(destination.latitude, 0, 8)
-        assert_almost_equal(abs(destination.longitude), 180, 8)
+        self.assertAlmostEqual(destination.latitude, 0, 8)
+        self.assertAlmostEqual(abs(destination.longitude), 180, 8)
 
     def test_should_compute_same_destination_as_other_libraries(self):
         distance = self.cls(54.972271)
         destination = distance.destination((-37.95103, 144.42487), 306.86816)
-        assert_almost_equal(destination.latitude, -37.6528177174, 10)
-        assert_almost_equal(destination.longitude, 143.9264976682, 10)
+        self.assertAlmostEqual(destination.latitude, -37.6528177174, 10)
+        self.assertAlmostEqual(destination.longitude, 143.9264976682, 10)
 
     def test_should_compute_same_destination_as_other_libraries(self):
         distance = self.cls(54.972271)
         destination = distance.destination((-37.95103, 144.42487), 306.86816)
-        assert_almost_equal(destination.latitude, -37.6528177174, 10)
-        assert_almost_equal(destination.longitude, 143.9264976682, 10)
+        self.assertAlmostEqual(destination.latitude, -37.6528177174, 10)
+        self.assertAlmostEqual(destination.longitude, 143.9264976682, 10)
 
     def test_should_get_distinct_results_for_different_ellipsoids(self):
-        results = []
-        for ellipsoid_name in ELLIPSOIDS.keys():
-            results.append(self.cls((0, 0), (0, 1), ellipsoid=ELLIPSOIDS[ellipsoid_name]))
+        results = [
+            self.cls((0, 0), (0, 1), ellipsoid=ELLIPSOIDS[ellipsoid_name])
+            for ellipsoid_name in ELLIPSOIDS.keys()
+        ]
+
+        self.assertFalse(any(results[x].kilometers == results[y].kilometers
+                             for x in range(len(results))
+                             for y in range(len(results))
+                             if x != y))
 
-        assert not any(results[x].kilometers == results[y].kilometers
-                       for x in range(len(results))
-                       for y in range(len(results))
-                       if x != y)
 
 class TestWhenComputingGeodesicDistance(CommonDistanceCases,
                                         unittest.TestCase):
@@ -399,10 +431,10 @@ class TestWhenComputingGeodesicDistance(CommonDistanceCases,
             (lat1, lon1, azi1, lat2, lon2, azi2, s12) = l
             p1, p2 = Point(lat1, lon1), Point(lat2, lon2)
             s12a = d.measure(p1, p2) * km
-            assert_almost_equal(s12a, s12, delta = 1e-8)
+            self.assertAlmostEqual(s12a, s12, delta = 1e-8)
             p = d.destination(p1, azi1, s12/km)
-            assert_almost_equal(p.latitude, p2.latitude, delta = 1e-13)
-            assert_almost_equal(p.longitude, p2.longitude, delta = 1e-12)
+            self.assertAlmostEqual(p.latitude, p2.latitude, delta = 1e-13)
+            self.assertAlmostEqual(p.longitude, p2.longitude, delta = 1e-12)
             p = d.destination(p2, azi2, -s12/km)
-            assert_almost_equal(p.latitude, p1.latitude, delta = 1e-13)
-            assert_almost_equal(p.longitude, p1.longitude, delta = 1e-12)
+            self.assertAlmostEqual(p.latitude, p1.latitude, delta = 1e-13)
+            self.assertAlmostEqual(p.longitude, p1.longitude, delta = 1e-12)
diff --git a/test/test_point.py b/test/test_point.py
index 4860779..0b5005c 100644
--- a/test/test_point.py
+++ b/test/test_point.py
@@ -6,6 +6,7 @@ import pickle
 import unittest
 import math
 import sys
+import warnings
 
 from geopy.compat import u
 from geopy.point import Point
@@ -85,14 +86,30 @@ class PointTestCase(unittest.TestCase):
         self.assertEqual(point.format_decimal('m'), "41.5, 81.0, 0.0m")
 
     def test_point_from_iterable(self):
-        self.assertEqual(Point(1, 2, 3), Point([1, 2, 3]))
-        self.assertEqual(Point(1, 2, 0), Point([1, 2]))
-        self.assertEqual(Point(1, 0, 0), Point([1]))
-        self.assertEqual(Point(0, 0, 0), Point([]))
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            self.assertEqual(Point(1, 2, 3), Point([1, 2, 3]))
+            self.assertEqual(Point(1, 2, 0), Point([1, 2]))
+            self.assertEqual(0, len(w))
+            self.assertEqual(Point(1, 0, 0), Point([1]))
+            self.assertEqual(1, len(w))  # a single latitude is discouraged
+            self.assertEqual(Point(0, 0, 0), Point([]))
+            self.assertEqual(1, len(w))
 
         with self.assertRaises(ValueError):
             Point([1, 2, 3, 4])
 
+    def test_point_from_single_number(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            # Point from a single number is probably a misuse,
+            # thus it's discouraged.
+            self.assertEqual((5, 0, 0), tuple(Point(5)))
+            self.assertEqual(1, len(w))
+            # But an explicit zero longitude is fine
+            self.assertEqual((5, 0, 0), tuple(Point(5, 0)))
+            self.assertEqual(1, len(w))
+
     def test_point_from_point(self):
         point = Point(self.lat, self.lon, self.alt)
         point_copy = Point(point)
@@ -105,22 +122,33 @@ class PointTestCase(unittest.TestCase):
         self.assertEqual((10, 11, 12), tuple(point))
 
     def test_point_degrees_are_normalized(self):
-        point = Point(95, 185, 375)
-        self.assertEqual((-85, -175, 375), tuple(point))
-        point = Point(-95, -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
-        self.assertEqual((1.0, 1.0, 1.0), tuple(math.copysign(1.0, x) for x in point))
-        point = Point(90, 180, 375)
-        self.assertEqual((90, 180, 375), tuple(point))
-        point = Point(-90, -180, 375)
-        self.assertEqual((-90, -180, 375), tuple(point))
-        point = Point(-270, -540, 375)
-        self.assertEqual((-90, -180, 375), tuple(point))
-        point = Point(270, 540, 375)
-        self.assertEqual((-90, -180, 375), tuple(point))
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            point = Point(95, 185, 375)
+            self.assertEqual((-85, -175, 375), tuple(point))
+            self.assertEqual(1, len(w))
+
+            point = Point(-95, -185, 375)
+            self.assertEqual((85, 175, 375), tuple(point))
+            self.assertEqual(2, len(w))
+
+            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
+            self.assertEqual((1.0, 1.0, 1.0), tuple(math.copysign(1.0, x) for x in point))
+            point = Point(90, 180, 375)
+            self.assertEqual((90, 180, 375), tuple(point))
+            point = Point(-90, -180, 375)
+            self.assertEqual((-90, -180, 375), tuple(point))
+            self.assertEqual(2, len(w))
+
+            point = Point(-270, -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))
 
     def test_point_degrees_normalization_does_not_lose_precision(self):
         if sys.float_info.mant_dig != 53:
@@ -145,10 +173,13 @@ 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.
-        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))
+        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))
 
     def test_unpacking(self):
         point = Point(self.lat, self.lon, self.alt)
@@ -206,6 +237,19 @@ class PointTestCase(unittest.TestCase):
         self.assertEqual(point.longitude, self.lon)
         self.assertEqual(point.altitude, self.alt)
 
+    def test_point_setitem_normalization(self):
+        point = Point()
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            point[0] = 100
+            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))
+
     def test_point_assign_coordinates(self):
         point = Point(self.lat + 10, self.lon + 10, self.alt + 10)
         point.latitude = self.lat