diff --git a/AUTHORS b/AUTHORS index 0a3699d516..6a7f22ada4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,8 @@ The PRIMARY AUTHORS are (and/or have been): * Claude Paroz * Anssi Kääriäinen * Florian Apolloner + * Jeremy Dunck + * Bryan Veloso More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -167,7 +169,6 @@ answer newbie questions, and generally made Django that much better: dready Maximillian Dornseif Daniel Duan - Jeremy Dunck Andrew Durdin dusk@woofle.net Andy Dustman diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 0fc9f3754a..11518193e7 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -24,7 +24,9 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE if test_func(request.user): return view_func(request, *args, **kwargs) path = request.build_absolute_uri() - resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) + # urlparse chokes on lazy objects in Python 3, force to str + resolved_login_url = force_str( + resolve_url(login_url or settings.LOGIN_URL)) # If the login url is the same scheme and net location then just # use the path as the "next" url. login_scheme, login_netloc = urlparse(resolved_login_url)[:2] @@ -33,7 +35,8 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE (not login_netloc or login_netloc == current_netloc)): path = request.get_full_path() from django.contrib.auth.views import redirect_to_login - return redirect_to_login(path, login_url, redirect_field_name) + return redirect_to_login( + path, resolved_login_url, redirect_field_name) return _wrapped_view return decorator diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 75b3ca4ece..08488237c7 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from django.contrib.auth import authenticate from django.contrib.auth.models import User -from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, identify_hasher +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -24,22 +24,22 @@ mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p class ReadOnlyPasswordHashWidget(forms.Widget): def render(self, name, value, attrs): encoded = value - - if not is_password_usable(encoded): - return "None" - final_attrs = self.build_attrs(attrs) - try: - hasher = identify_hasher(encoded) - except ValueError: - summary = mark_safe("Invalid password format or unknown hashing algorithm.") + if encoded == '' or encoded == UNUSABLE_PASSWORD: + summary = mark_safe("%s" % ugettext("No password set.")) else: - summary = format_html_join('', - "{0}: {1} ", - ((ugettext(key), value) - for key, value in hasher.safe_summary(encoded).items()) - ) + try: + hasher = identify_hasher(encoded) + except ValueError: + summary = mark_safe("%s" % ugettext( + "Invalid password format or unknown hashing algorithm.")) + else: + summary = format_html_join('', + "{0}: {1} ", + ((ugettext(key), value) + for key, value in hasher.safe_summary(encoded).items()) + ) return format_html("{1}", flatatt(final_attrs), summary) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index bd0c6778c9..c628059d34 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -28,7 +28,13 @@ def reset_hashers(**kwargs): def is_password_usable(encoded): - return (encoded is not None and encoded != UNUSABLE_PASSWORD) + if encoded is None or encoded == UNUSABLE_PASSWORD: + return False + try: + hasher = identify_hasher(encoded) + except ValueError: + return False + return True def check_password(password, encoded, setter=None, preferred='default'): diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 0b555e9f77..38f21cd7c0 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -240,23 +240,29 @@ class UserChangeFormTest(TestCase): # Just check we can create it form = MyUserForm({}) + def test_unsuable_password(self): + user = User.objects.get(username='empty_password') + user.set_unusable_password() + user.save() + form = UserChangeForm(instance=user) + self.assertIn(_("No password set."), form.as_table()) + def test_bug_17944_empty_password(self): user = User.objects.get(username='empty_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("No password set."), form.as_table()) def test_bug_17944_unmanageable_password(self): user = User.objects.get(username='unmanageable_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) def test_bug_17944_unknown_password_algorithm(self): user = User.objects.get(username='unknown_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) @skipIfCustomUser diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index 673263b566..d867a57d98 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -100,6 +100,10 @@ class TestUtilsHashPass(unittest.TestCase): self.assertRaises(ValueError, doit) self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash") + def test_bad_encoded(self): + self.assertFalse(is_password_usable('letmein_badencoded')) + self.assertFalse(is_password_usable('')) + def test_low_level_pkbdf2(self): hasher = PBKDF2PasswordHasher() encoded = hasher.encode('letmein', 'seasalt') diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index f7af420a8d..2b8924d92e 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -90,8 +90,6 @@ class BaseSpatialOperations(object): # For quoting column values, rather than columns. def geo_quote_name(self, name): - if isinstance(name, six.text_type): - name = name.encode('ascii') return "'%s'" % name # GeometryField operations diff --git a/django/contrib/gis/db/backends/util.py b/django/contrib/gis/db/backends/util.py index 648fcfe963..2fc9123d26 100644 --- a/django/contrib/gis/db/backends/util.py +++ b/django/contrib/gis/db/backends/util.py @@ -3,20 +3,6 @@ A collection of utility routines and classes used by the spatial backends. """ -from django.utils import six - -def gqn(val): - """ - The geographic quote name function; used for quoting tables and - geometries (they use single rather than the double quotes of the - backend quotename function). - """ - if isinstance(val, six.string_types): - if isinstance(val, six.text_type): val = val.encode('ascii') - return "'%s'" % val - else: - return str(val) - class SpatialOperation(object): """ Base class for generating spatial SQL. diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 71d22a0a27..22394a2888 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -181,7 +181,11 @@ class DataSourceTest(unittest.TestCase): # Making sure the SpatialReference is as expected. if hasattr(source, 'srs_wkt'): - self.assertEqual(source.srs_wkt, g.srs.wkt) + self.assertEqual( + source.srs_wkt, + # Depending on lib versions, WGS_84 might be WGS_1984 + g.srs.wkt.replace('SPHEROID["WGS_84"', 'SPHEROID["WGS_1984"') + ) def test06_spatial_filter(self): "Testing the Layer.spatial_filter property." diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index a0b2593605..dda22036e3 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -1,3 +1,4 @@ +import json from binascii import b2a_hex try: from django.utils.six.moves import cPickle as pickle @@ -111,8 +112,9 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): for g in self.geometries.json_geoms: geom = OGRGeometry(g.wkt) if not hasattr(g, 'not_equal'): - self.assertEqual(g.json, geom.json) - self.assertEqual(g.json, geom.geojson) + # Loading jsons to prevent decimal differences + self.assertEqual(json.loads(g.json), json.loads(geom.json)) + self.assertEqual(json.loads(g.json), json.loads(geom.geojson)) self.assertEqual(OGRGeometry(g.wkt), OGRGeometry(geom.json)) def test02_points(self): diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index a4f5adf4d0..aed6cf366c 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -110,7 +110,7 @@ def geos_version_info(): is a release candidate (and what number release candidate), and the C API version. """ - ver = geos_version() + ver = geos_version().decode() m = version_regex.match(ver) if not m: raise GEOSException('Could not parse version info string "%s"' % ver) return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor')) diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py index 69e50e6b3f..820cdfa5a4 100644 --- a/django/contrib/gis/geos/mutable_list.py +++ b/django/contrib/gis/geos/mutable_list.py @@ -215,15 +215,18 @@ class ListMixin(object): "Standard list reverse method" self[:] = self[-1::-1] - def sort(self, cmp=cmp, key=None, reverse=False): + def sort(self, cmp=None, key=None, reverse=False): "Standard list sort method" if key: temp = [(key(v),v) for v in self] - temp.sort(cmp=cmp, key=lambda x: x[0], reverse=reverse) + temp.sort(key=lambda x: x[0], reverse=reverse) self[:] = [v[1] for v in temp] else: temp = list(self) - temp.sort(cmp=cmp, reverse=reverse) + if cmp is not None: + temp.sort(cmp=cmp, reverse=reverse) + else: + temp.sort(reverse=reverse) self[:] = temp ### Private routines ### diff --git a/django/contrib/gis/geos/tests/__init__.py b/django/contrib/gis/geos/tests/__init__.py index ccf960c68f..6b715d8c59 100644 --- a/django/contrib/gis/geos/tests/__init__.py +++ b/django/contrib/gis/geos/tests/__init__.py @@ -16,7 +16,8 @@ test_suites = [ def suite(): "Builds a test suite for the GEOS tests." s = TestSuite() - map(s.addTest, test_suites) + for suite in test_suites: + s.addTest(suite) return s def run(verbosity=1): diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index d621c6b4d4..7300ab9c63 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,4 +1,5 @@ import ctypes +import json import random from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, @@ -204,8 +205,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): for g in self.geometries.json_geoms: geom = GEOSGeometry(g.wkt) if not hasattr(g, 'not_equal'): - self.assertEqual(g.json, geom.json) - self.assertEqual(g.json, geom.geojson) + # Loading jsons to prevent decimal differences + self.assertEqual(json.loads(g.json), json.loads(geom.json)) + self.assertEqual(json.loads(g.json), json.loads(geom.geojson)) self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json)) def test_fromfile(self): diff --git a/django/contrib/gis/geos/tests/test_mutable_list.py b/django/contrib/gis/geos/tests/test_mutable_list.py index cd174d7cfa..675505f0f9 100644 --- a/django/contrib/gis/geos/tests/test_mutable_list.py +++ b/django/contrib/gis/geos/tests/test_mutable_list.py @@ -55,14 +55,14 @@ class ListMixinTest(unittest.TestCase): def lists_of_len(self, length=None): if length is None: length = self.limit - pl = range(length) + pl = list(range(length)) return pl, self.listType(pl) def limits_plus(self, b): return range(-self.limit - b, self.limit + b) def step_range(self): - return range(-1 - self.limit, 0) + range(1, 1 + self.limit) + return list(range(-1 - self.limit, 0)) + list(range(1, 1 + self.limit)) def test01_getslice(self): 'Slice retrieval' @@ -160,13 +160,13 @@ class ListMixinTest(unittest.TestCase): del pl[i:j] del ul[i:j] self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[i:j:k] del ul[i:j:k] self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[:i:k] del ul[:i:k] @@ -177,7 +177,7 @@ class ListMixinTest(unittest.TestCase): del ul[i::k] self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[::k] del ul[::k] @@ -320,7 +320,7 @@ class ListMixinTest(unittest.TestCase): pl.sort() ul.sort() self.assertEqual(pl[:], ul[:], 'sort') - mid = pl[len(pl) / 2] + mid = pl[len(pl) // 2] pl.sort(key=lambda x: (mid-x)**2) ul.sort(key=lambda x: (mid-x)**2) self.assertEqual(pl[:], ul[:], 'sort w/ key') @@ -330,7 +330,7 @@ class ListMixinTest(unittest.TestCase): pl.sort(reverse=True) ul.sort(reverse=True) self.assertEqual(pl[:], ul[:], 'sort w/ reverse') - mid = pl[len(pl) / 2] + mid = pl[len(pl) // 2] pl.sort(key=lambda x: (mid-x)**2) ul.sort(key=lambda x: (mid-x)**2) self.assertEqual(pl[:], ul[:], 'sort w/ key') @@ -338,7 +338,7 @@ class ListMixinTest(unittest.TestCase): def test_12_arithmetic(self): 'Arithmetic' pl, ul = self.lists_of_len() - al = range(10,14) + al = list(range(10,14)) self.assertEqual(list(pl + al), list(ul + al), 'add') self.assertEqual(type(ul), type(ul + al), 'type of add result') self.assertEqual(list(al + pl), list(al + ul), 'radd') diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index 5cdc68a74d..7f7a0111f1 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -8,9 +8,11 @@ from django.utils import unittest test_srs = ({'srid' : 4326, 'auth_name' : ('EPSG', True), 'auth_srid' : 4326, - 'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', - 'srtext14' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', - 'proj4' : '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + # Only the beginning, because there are differences depending on installed libs + 'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84"', + 'proj4' : ['+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + # +ellps=WGS84 has been removed in the 4326 proj string in proj-4.8 + '+proj=longlat +datum=WGS84 +no_defs '], 'spheroid' : 'WGS 84', 'name' : 'WGS 84', 'geographic' : True, 'projected' : False, 'spatialite' : True, 'ellipsoid' : (6378137.0, 6356752.3, 298.257223563), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) @@ -19,9 +21,9 @@ test_srs = ({'srid' : 4326, {'srid' : 32140, 'auth_name' : ('EPSG', False), 'auth_srid' : 32140, - 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]', - 'srtext14': 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],AUTHORITY["EPSG","32140"],AXIS["X",EAST],AXIS["Y",NORTH]]', - 'proj4' : '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"', + 'proj4' : ['+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central', 'geographic' : False, 'projected' : True, 'spatialite' : False, 'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) @@ -51,17 +53,12 @@ class SpatialRefSysTest(unittest.TestCase): # No proj.4 and different srtext on oracle backends :( if postgis: - if connection.ops.spatial_version >= (1, 4, 0): - srtext = sd['srtext14'] - else: - srtext = sd['srtext'] - self.assertEqual(srtext, srs.wkt) - self.assertEqual(sd['proj4'], srs.proj4text) + self.assertTrue(srs.wkt.startswith(sd['srtext'])) + self.assertTrue(srs.proj4text in sd['proj4']) @no_mysql def test02_osr(self): "Testing getting OSR objects from SpatialRefSys model objects." - from django.contrib.gis.gdal import GDAL_VERSION for sd in test_srs: sr = SpatialRefSys.objects.get(srid=sd['srid']) self.assertEqual(True, sr.spheroid.startswith(sd['spheroid'])) @@ -76,15 +73,10 @@ class SpatialRefSysTest(unittest.TestCase): # Testing the SpatialReference object directly. if postgis or spatialite: srs = sr.srs - if GDAL_VERSION <= (1, 8): - self.assertEqual(sd['proj4'], srs.proj4) + self.assertTrue(srs.proj4 in sd['proj4']) # No `srtext` field in the `spatial_ref_sys` table in SpatiaLite if not spatialite: - if connection.ops.spatial_version >= (1, 4, 0): - srtext = sd['srtext14'] - else: - srtext = sd['srtext'] - self.assertEqual(srtext, srs.wkt) + self.assertTrue(srs.wkt.startswith(sd['srtext'])) @no_mysql def test03_ellipsoid(self): diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index e445d07a61..7a32a3dac7 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -223,18 +223,17 @@ class WSGIHandler(base.BaseHandler): set_script_prefix(base.get_script_name(environ)) signals.request_started.send(sender=self.__class__) try: - try: - request = self.request_class(environ) - except UnicodeDecodeError: - logger.warning('Bad Request (UnicodeDecodeError)', - exc_info=sys.exc_info(), - extra={ - 'status_code': 400, - } - ) - response = http.HttpResponseBadRequest() - else: - response = self.get_response(request) + request = self.request_class(environ) + except UnicodeDecodeError: + logger.warning('Bad Request (UnicodeDecodeError)', + exc_info=sys.exc_info(), + extra={ + 'status_code': 400, + } + ) + response = http.HttpResponseBadRequest() + else: + response = self.get_response(request) finally: signals.request_finished.send(sender=self.__class__) diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index c067c9c322..9c24701d2e 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -1,4 +1,5 @@ from optparse import make_option +from datetime import datetime import os import re import sys @@ -90,10 +91,12 @@ class Command(BaseCommand): self.stdout.write("Validating models...\n\n") self.validate(display_num_errors=True) self.stdout.write(( + "%(started_at)s\n" "Django version %(version)s, using settings %(settings)r\n" "Development server is running at http://%(addr)s:%(port)s/\n" "Quit the server with %(quit_command)s.\n" ) % { + "started_at": datetime.now().strftime('%B %d, %Y - %X'), "version": self.get_version(), "settings": settings.SETTINGS_MODULE, "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr, diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index cec3b04f7e..4043014b8e 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -37,6 +37,7 @@ from django.db.backends.mysql.client import DatabaseClient from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation +from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.safestring import SafeBytes, SafeText from django.utils import six @@ -390,7 +391,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): if settings_dict['NAME']: kwargs['db'] = settings_dict['NAME'] if settings_dict['PASSWORD']: - kwargs['passwd'] = settings_dict['PASSWORD'] + kwargs['passwd'] = force_str(settings_dict['PASSWORD']) if settings_dict['HOST'].startswith('/'): kwargs['unix_socket'] = settings_dict['HOST'] elif settings_dict['HOST']: diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index f6f534da8c..c8b88d5619 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -13,6 +13,7 @@ from django.db.backends.postgresql_psycopg2.client import DatabaseClient from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection +from django.utils.encoding import force_str from django.utils.log import getLogger from django.utils.safestring import SafeText, SafeBytes from django.utils import six @@ -172,7 +173,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): if settings_dict['USER']: conn_params['user'] = settings_dict['USER'] if settings_dict['PASSWORD']: - conn_params['password'] = settings_dict['PASSWORD'] + conn_params['password'] = force_str(settings_dict['PASSWORD']) if settings_dict['HOST']: conn_params['host'] = settings_dict['HOST'] if settings_dict['PORT']: diff --git a/django/forms/fields.py b/django/forms/fields.py index 7f0d26d1aa..124e4f669a 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -199,7 +199,7 @@ class CharField(Field): def widget_attrs(self, widget): attrs = super(CharField, self).widget_attrs(widget) - if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + if self.max_length is not None and isinstance(widget, TextInput): # The HTML attribute is maxlength, not max_length. attrs.update({'maxlength': str(self.max_length)}) return attrs diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 7651efccd0..763da0cff2 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -260,10 +260,17 @@ class Input(Widget): final_attrs['value'] = force_text(self._format_value(value)) return format_html('', flatatt(final_attrs)) + class TextInput(Input): input_type = 'text' -class PasswordInput(Input): + def __init__(self, attrs=None): + if attrs is not None: + self.input_type = attrs.pop('type', self.input_type) + super(TextInput, self).__init__(attrs) + + +class PasswordInput(TextInput): input_type = 'password' def __init__(self, attrs=None, render_value=False): @@ -400,9 +407,8 @@ class Textarea(Widget): flatatt(final_attrs), force_text(value)) -class DateInput(Input): - input_type = 'text' +class DateInput(TextInput): def __init__(self, attrs=None, format=None): super(DateInput, self).__init__(attrs) if format: @@ -431,9 +437,8 @@ class DateInput(Input): pass return super(DateInput, self)._has_changed(self._format_value(initial), data) -class DateTimeInput(Input): - input_type = 'text' +class DateTimeInput(TextInput): def __init__(self, attrs=None, format=None): super(DateTimeInput, self).__init__(attrs) if format: @@ -462,9 +467,8 @@ class DateTimeInput(Input): pass return super(DateTimeInput, self)._has_changed(self._format_value(initial), data) -class TimeInput(Input): - input_type = 'text' +class TimeInput(TextInput): def __init__(self, attrs=None, format=None): super(TimeInput, self).__init__(attrs) if format: diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 305b20e1c4..c9e8d73c82 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -105,7 +105,7 @@ class CsrfViewMiddleware(object): if getattr(callback, 'csrf_exempt', False): return None - # Assume that anything not defined as 'safe' by RC2616 needs protection + # Assume that anything not defined as 'safe' by RFC2616 needs protection if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): if getattr(request, '_dont_enforce_csrf_checks', False): # Mechanism to turn off CSRF checks for test suite. diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index cb2ecd26d8..ea1dd0281e 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -531,11 +531,9 @@ def cycle(parser, token): The optional flag "silent" can be used to prevent the cycle declaration from returning any value:: - {% cycle 'row1' 'row2' as rowcolors silent %}{# no value here #} {% for o in some_list %} - {# first value will be "row1" #} - ... - + {% cycle 'row1' 'row2' as rowcolors silent %} + {% include "subtemplate.html " %} {% endfor %} """ diff --git a/django/utils/html_parser.py b/django/utils/html_parser.py index d7311f253b..6ccb665249 100644 --- a/django/utils/html_parser.py +++ b/django/utils/html_parser.py @@ -5,8 +5,7 @@ import sys current_version = sys.version_info use_workaround = ( - (current_version < (2, 6, 8)) or - (current_version >= (2, 7) and current_version < (2, 7, 3)) or + (current_version < (2, 7, 3)) or (current_version >= (3, 0) and current_version < (3, 2, 3)) ) diff --git a/docs/contents.txt b/docs/contents.txt index 9bf0d685c4..736e1f62bf 100644 --- a/docs/contents.txt +++ b/docs/contents.txt @@ -28,14 +28,3 @@ Indices, glossary and tables * :ref:`genindex` * :ref:`modindex` * :ref:`glossary` - -Deprecated/obsolete documentation -================================= - -The following documentation covers features that have been deprecated or that -have been replaced in newer versions of Django. - -.. toctree:: - :maxdepth: 2 - - obsolete/index diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 8ec7491903..ea6aa2e74e 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -91,8 +91,7 @@ The dynamically-generated admin site is ugly! How can I change it? We like it, but if you don't agree, you can modify the admin site's presentation by editing the CSS stylesheet and/or associated image files. The site is built using semantic HTML and plenty of CSS hooks, so any changes you'd -like to make should be possible by editing the stylesheet. We've got a -:doc:`guide to the CSS used in the admin ` to get you started. +like to make should be possible by editing the stylesheet. What browsers are supported for using the admin? ------------------------------------------------ @@ -104,5 +103,5 @@ There *may* be minor stylistic differences between supported browsers—for example, some browsers may not support rounded corners. These are considered acceptable variations in rendering. -.. _YUI's A-grade: http://yuilibrary.com/yui/docs/tutorials/gbs/ +.. _YUI's A-grade: http://yuilibrary.com/yui/docs/tutorials/gbs/ diff --git a/docs/index.txt b/docs/index.txt index 3f4e30385c..8b29c95fa2 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -5,7 +5,7 @@ Django documentation ==================== -.. rubric:: Everything you need to know about Django (and then some). +.. rubric:: Everything you need to know about Django. Getting help ============ diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 2faace99a5..ca56d36880 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -379,6 +379,34 @@ Florian Apolloner .. _Graz University of Technology: http://tugraz.at/ .. _Ubuntuusers webteam: http://wiki.ubuntuusers.de/ubuntuusers/Webteam +Jeremy Dunck + Jeremy was rescued from corporate IT drudgery by Free Software and, in part, + Django. Many of Jeremy's interests center around access to information. + + Jeremy was the lead developer of Pegasus News, one of the first uses of + Django outside World Online, and has since joined Votizen, a startup intent + on reducing the influence of money in politics. + + He serves as DSF Secretary, organizes and helps organize sprints, cares + about the health and equity of the Django community. He has gone an + embarrassingly long time without a working blog. + + Jeremy lives in Mountain View, CA, USA. + +`Bryan Veloso`_ + Bryan found Django 0.96 through a fellow designer who was evangelizing + its use. It was his first foray outside of the land that was PHP-based + templating. Although he has only ever used Django for personal projects, + it is the very reason he considers himself a designer/developer + hybrid and is working to further design within the Django community. + + Bryan works as a designer at GitHub by day, and masquerades as a `vlogger`_ + and `shoutcaster`_ in the after-hours. Bryan lives in Los Angeles, CA, USA. + +.. _bryan veloso: http://avalonstar.com/ +.. _vlogger: http://youtube.com/bryanveloso/ +.. _shoutcaster: http://twitch.tv/vlogalonstar/ + Specialists ----------- @@ -403,16 +431,6 @@ Ian Kelly Matt Boersma Matt is also responsible for Django's Oracle support. -Jeremy Dunck - Jeremy is the lead developer of Pegasus News, a personalized local site based - in Dallas, Texas. An early contributor to Greasemonkey and Django, he sees - technology as a tool for communication and access to knowledge. - - Jeremy helped kick off GeoDjango development, and is mostly responsible for - the serious speed improvements that signals received in Django 1.0. - - Jeremy lives in Dallas, Texas, USA. - `Simon Meers`_ Simon discovered Django 0.96 during his Computer Science PhD research and has been developing with it full-time ever since. His core code diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index cc793c8129..ea4b18de03 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -67,8 +67,7 @@ different needs: whathaveyou. * Finally, there's some "specialized" documentation not usually relevant to - most developers. This includes the :doc:`release notes `, - :doc:`documentation of obsolete features `, + most developers. This includes the :doc:`release notes ` and :doc:`internals documentation ` for those who want to add code to Django itself, and a :doc:`few other things that simply don't fit elsewhere `. diff --git a/docs/obsolete/_images/formrow.png b/docs/obsolete/_images/formrow.png deleted file mode 100644 index 164dd262b5..0000000000 Binary files a/docs/obsolete/_images/formrow.png and /dev/null differ diff --git a/docs/obsolete/_images/module.png b/docs/obsolete/_images/module.png deleted file mode 100644 index 6acda97809..0000000000 Binary files a/docs/obsolete/_images/module.png and /dev/null differ diff --git a/docs/obsolete/_images/objecttools_01.png b/docs/obsolete/_images/objecttools_01.png deleted file mode 100644 index 0aba8163d6..0000000000 Binary files a/docs/obsolete/_images/objecttools_01.png and /dev/null differ diff --git a/docs/obsolete/_images/objecttools_02.png b/docs/obsolete/_images/objecttools_02.png deleted file mode 100644 index 06a854009b..0000000000 Binary files a/docs/obsolete/_images/objecttools_02.png and /dev/null differ diff --git a/docs/obsolete/admin-css.txt b/docs/obsolete/admin-css.txt deleted file mode 100644 index f4cca549b4..0000000000 --- a/docs/obsolete/admin-css.txt +++ /dev/null @@ -1,186 +0,0 @@ -====================================== -Customizing the Django admin interface -====================================== - -.. warning:: - - The design of the admin has changed somewhat since this document was - written, and parts may not apply any more. This document is no longer - maintained since an official API for customizing the Django admin interface - is in development. - -Django's dynamic admin interface gives you a fully-functional admin for free -with no hand-coding required. The dynamic admin is designed to be -production-ready, not just a starting point, so you can use it as-is on a real -site. While the underlying format of the admin pages is built in to Django, you -can customize the look and feel by editing the admin stylesheet and images. - -Here's a quick and dirty overview some of the main styles and classes used in -the Django admin CSS. - -Modules -======= - -The ``.module`` class is a basic building block for grouping content in the -admin. It's generally applied to a ``div`` or a ``fieldset``. It wraps the content -group in a box and applies certain styles to the elements within. An ``h2`` -within a ``div.module`` will align to the top of the ``div`` as a header for the -whole group. - -.. image:: _images/module.png - :alt: Example use of module class on admin homepage - -Column Types -============ - -.. note:: - - All admin pages (except the dashboard) are fluid-width. All fixed-width - classes from previous Django versions have been removed. - -The base template for each admin page has a block that defines the column -structure for the page. This sets a class on the page content area -(``div#content``) so everything on the page knows how wide it should be. There -are three column types available. - -colM - This is the default column setting for all pages. The "M" stands for "main". - Assumes that all content on the page is in one main column - (``div#content-main``). -colMS - This is for pages with one main column and a sidebar on the right. The "S" - stands for "sidebar". Assumes that main content is in ``div#content-main`` - and sidebar content is in ``div#content-related``. This is used on the main - admin page. -colSM - Same as above, with the sidebar on the left. The source order of the columns - doesn't matter. - -For instance, you could stick this in a template to make a two-column page with -the sidebar on the right: - -.. code-block:: html+django - - {% block coltype %}colMS{% endblock %} - -Text Styles -=========== - -Font Sizes ----------- - -Most HTML elements (headers, lists, etc.) have base font sizes in the stylesheet -based on context. There are three classes are available for forcing text to a -certain size in any context. - -small - 11px -tiny - 10px -mini - 9px (use sparingly) - -Font Styles and Alignment -------------------------- - -There are also a few styles for styling text. - -.quiet - Sets font color to light gray. Good for side notes in instructions. Combine - with ``.small`` or ``.tiny`` for sheer excitement. -.help - This is a custom class for blocks of inline help text explaining the - function of form elements. It makes text smaller and gray, and when applied - to ``p`` elements within ``.form-row`` elements (see Form Styles below), - it will offset the text to align with the form field. Use this for help - text, instead of ``small quiet``. It works on other elements, but try to - put the class on a ``p`` whenever you can. -.align-left - It aligns the text left. Only works on block elements containing inline - elements. -.align-right - Are you paying attention? -.nowrap - Keeps text and inline objects from wrapping. Comes in handy for table - headers you want to stay on one line. - -Floats and Clears ------------------ - -float-left - floats left -float-right - floats right -clear - clears all - -Object Tools -============ - -Certain actions which apply directly to an object are used in form and -changelist pages. These appear in a "toolbar" row above the form or changelist, -to the right of the page. The tools are wrapped in a ``ul`` with the class -``object-tools``. There are two custom tool types which can be defined with an -additional class on the ``a`` for that tool. These are ``.addlink`` and -``.viewsitelink``. - -Example from a changelist page: - -.. code-block:: html+django - - - -.. image:: _images/objecttools_01.png - :alt: Object tools on a changelist page - -and from a form page: - -.. code-block:: html+django - - - -.. image:: _images/objecttools_02.png - :alt: Object tools on a form page - -Form Styles -=========== - -Fieldsets ---------- - -Admin forms are broken up into groups by ``fieldset`` elements. Each form fieldset -should have a class ``.module``. Each fieldset should have a header ``h2`` within the -fieldset at the top (except the first group in the form, and in some cases where the -group of fields doesn't have a logical label). - -Each fieldset can also take extra classes in addition to ``.module`` to apply -appropriate formatting to the group of fields. - -.aligned - This will align the labels and inputs side by side on the same line. -.wide - Used in combination with ``.aligned`` to widen the space available for the - labels. - -Form Rows ---------- - -Each row of the form (within the ``fieldset``) should be enclosed in a ``div`` -with class ``form-row``. If the field in the row is required, a class of -``required`` should also be added to the ``div.form-row``. - -.. image:: _images/formrow.png - :alt: Example use of form-row class - -Labels ------- - -Form labels should always precede the field, except in the case -of checkboxes and radio buttons, where the ``input`` should come first. Any -explanation or help text should follow the ``label`` in a ``p`` with class -``.help``. diff --git a/docs/obsolete/index.txt b/docs/obsolete/index.txt deleted file mode 100644 index ddc86237cc..0000000000 --- a/docs/obsolete/index.txt +++ /dev/null @@ -1,12 +0,0 @@ -Deprecated/obsolete documentation -================================= - -These documents cover features that have been deprecated or that have been -replaced in newer versions of Django. They're preserved here for folks using old -versions of Django or those still using deprecated APIs. No new code based on -these APIs should be written. - -.. toctree:: - :maxdepth: 1 - - admin-css \ No newline at end of file diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index eab314a4cd..1935cb23bc 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -126,8 +126,9 @@ provided for each widget will be rendered exactly the same:: On a real Web page, you probably don't want every widget to look the same. You might want a larger input element for the comment, and you might want the -'name' widget to have some special CSS class. To do this, you use the -:attr:`Widget.attrs` argument when creating the widget: +'name' widget to have some special CSS class. It is also possible to specify +the 'type' attribute to take advantage of the new HTML5 input types. To do +this, you use the :attr:`Widget.attrs` argument when creating the widget: For example:: @@ -245,7 +246,7 @@ commonly used groups of widgets: Date input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: DateInput.format @@ -262,7 +263,7 @@ commonly used groups of widgets: Date/time input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: DateTimeInput.format @@ -279,7 +280,7 @@ commonly used groups of widgets: Time input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: TimeInput.format diff --git a/docs/releases/index.txt b/docs/releases/index.txt index fa55a4d206..2329d1effa 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -14,6 +14,7 @@ up to and including the new version. Final releases ============== +.. _development_release_notes: 1.5 release ----------- diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 890c5e3195..39b9a93c04 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -257,15 +257,14 @@ Installing the development version If you decide to use the latest development version of Django, you'll want to pay close attention to `the development timeline`_, - and you'll want to keep an eye on `the list of - backwards-incompatible changes`_. This will help you stay on top - of any new features you might want to use, as well as any changes + and you'll want to keep an eye on the :ref:`release notes for the + upcoming release `. This will help you stay + on top of any new features you might want to use, as well as any changes you'll need to make to your code when updating your copy of Django. (For stable releases, any necessary changes are documented in the release notes.) .. _the development timeline: https://code.djangoproject.com/timeline -.. _the list of backwards-incompatible changes: https://code.djangoproject.com/wiki/BackwardsIncompatibleChanges If you'd like to be able to update your Django code occasionally with the latest bug fixes and improvements, follow these instructions: diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index e53b02032e..cfa298253c 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -401,6 +401,19 @@ class BackendTestCase(TestCase): self.assertEqual(list(cursor.fetchmany(2)), [('Jane', 'Doe'), ('John', 'Doe')]) self.assertEqual(list(cursor.fetchall()), [('Mary', 'Agnelline'), ('Peter', 'Parker')]) + def test_unicode_password(self): + old_password = connection.settings_dict['PASSWORD'] + connection.settings_dict['PASSWORD'] = "françois" + try: + cursor = connection.cursor() + except backend.Database.DatabaseError: + # As password is probably wrong, a database exception is expected + pass + except Exception as e: + self.fail("Unexpected error raised with unicode password: %s" % e) + finally: + connection.settings_dict['PASSWORD'] = old_password + def test_database_operations_helper_class(self): # Ticket #13630 self.assertTrue(hasattr(connection, 'ops')) diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 544ca41642..104144b288 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -31,9 +31,9 @@ class FormsWidgetTestCase(TestCase): self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '') # You can also pass 'attrs' to the constructor: - w = TextInput(attrs={'class': 'fun'}) - self.assertHTMLEqual(w.render('email', ''), '') - self.assertHTMLEqual(w.render('email', 'foo@example.com'), '') + w = TextInput(attrs={'class': 'fun', 'type': 'email'}) + self.assertHTMLEqual(w.render('email', ''), '') + self.assertHTMLEqual(w.render('email', 'foo@example.com'), '') # 'attrs' passed to render() get precedence over those passed to the constructor: w = TextInput(attrs={'class': 'pretty'}) @@ -915,8 +915,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), '') # Use 'format' to change the way a value is displayed. - w = DateTimeInput(format='%d/%m/%Y %H:%M') - self.assertHTMLEqual(w.render('date', d), '') + w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}) + self.assertHTMLEqual(w.render('date', d), '') self.assertFalse(w._has_changed(d, '17/09/2007 12:51')) # Make sure a custom format works with _has_changed. The hidden input will use @@ -938,8 +938,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('date', '2007-09-17'), '') # Use 'format' to change the way a value is displayed. - w = DateInput(format='%d/%m/%Y') - self.assertHTMLEqual(w.render('date', d), '') + w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) + self.assertHTMLEqual(w.render('date', d), '') self.assertFalse(w._has_changed(d, '17/09/2007')) # Make sure a custom format works with _has_changed. The hidden input will use @@ -963,8 +963,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('time', '13:12:11'), '') # Use 'format' to change the way a value is displayed. - w = TimeInput(format='%H:%M') - self.assertHTMLEqual(w.render('time', t), '') + w = TimeInput(format='%H:%M', attrs={'type': 'time'}) + self.assertHTMLEqual(w.render('time', t), '') self.assertFalse(w._has_changed(t, '12:51')) # Make sure a custom format works with _has_changed. The hidden input will use diff --git a/tests/regressiontests/utils/os_utils.py b/tests/regressiontests/utils/os_utils.py index a78f348cf5..a205d67431 100644 --- a/tests/regressiontests/utils/os_utils.py +++ b/tests/regressiontests/utils/os_utils.py @@ -1,21 +1,26 @@ +import os + from django.utils import unittest from django.utils._os import safe_join class SafeJoinTests(unittest.TestCase): def test_base_path_ends_with_sep(self): + drive, path = os.path.splitdrive(safe_join("/abc/", "abc")) self.assertEqual( - safe_join("/abc/", "abc"), - "/abc/abc", + path, + "{0}abc{0}abc".format(os.path.sep) ) def test_root_path(self): + drive, path = os.path.splitdrive(safe_join("/", "path")) self.assertEqual( - safe_join("/", "path"), - "/path", + path, + "{0}path".format(os.path.sep), ) + drive, path = os.path.splitdrive(safe_join("/", "")) self.assertEqual( - safe_join("/", ""), - "/", + path, + os.path.sep, )