Repository: django-extensions/django-extensions

Issue:
read_default_file not handled correctly
read_default_file is used by reset_db and drop_test_database

They process read_default_file themselves differently from the way django does, specifically:
- Does not expand user home directories
- Expects that a [client] section will be present if read_default_file is specified and throws an exception if not
- Expects all of user, password, database will be present if read_default_file is specified and throws an exception if one or more is not present
- Ignores any socket/host/port options in the config file

Incidentally, sqlcreate() and sqldsn() have their own handling that appears to not use read_default_file at all (although they don't make DB changes so are less likely to do unexpected things)



Discussion:
Unfortunately, the handing of read_default_file is in the mysql C libraries, not in django so it's not possible to hook into any django code to ensure the behaviour matches up and you would have to replicate the C library functionality.

You could try to operate using the django connection:
- Mysql will allow you to drop an open database so you could connect with django first then SELECT DATABASE(); to determine what to drop.
- If the database doesn't already exist then there's no way to know what it was trying to connect to in the first place (the exception thrown by django is simply `django.db.utils.OperationalError: (1046, 'No database selected')`) so you are stuck


Resolving patch:
diff --git a/django_extensions/management/commands/reset_db.py b/django_extensions/management/commands/reset_db.py
--- a/django_extensions/management/commands/reset_db.py
+++ b/django_extensions/management/commands/reset_db.py
@@ -7,8 +7,9 @@
 import django
 from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
-from six.moves import configparser, input
+from six.moves import input
 
+from django_extensions.management.mysql import parse_mysql_cnf
 from django_extensions.management.utils import signalcommand
 
 
@@ -57,15 +58,9 @@ def handle(self, *args, **options):
 
         engine = dbinfo.get('ENGINE').split('.')[-1]
 
-        user = password = database_name = ''
+        user = password = database_name = database_host = database_port = ''
         if engine == 'mysql':
-            read_default_file = dbinfo.get('OPTIONS', {}).get('read_default_file')
-            if read_default_file:
-                config = configparser.ConfigParser()
-                config.read(read_default_file)
-                user = config.get('client', 'user')
-                password = config.get('client', 'password')
-                database_name = config.get('client', 'database')
+            (user, password, database_name, database_host, database_port) = parse_mysql_cnf(dbinfo)
 
         user = options.get('user') or dbinfo.get('USER') or user
         password = options.get('password') or dbinfo.get('PASSWORD') or password
@@ -75,8 +70,8 @@ def handle(self, *args, **options):
         if database_name == '':
             raise CommandError("You need to specify DATABASE_NAME in your Django settings file.")
 
-        database_host = dbinfo.get('HOST')
-        database_port = dbinfo.get('PORT')
+        database_host = dbinfo.get('HOST') or database_host
+        database_port = dbinfo.get('PORT') or database_port
 
         verbosity = int(options.get('verbosity', 1))
         if options.get('interactive'):
diff --git a/django_extensions/management/mysql.py b/django_extensions/management/mysql.py
new file mode 100644
--- /dev/null
+++ b/django_extensions/management/mysql.py
@@ -0,0 +1,42 @@
+from six.moves import configparser
+
+
+def parse_mysql_cnf(dbinfo):
+    """
+    Attempt to parse mysql database config file for connection settings.
+    Ideally we would hook into django's code to do this, but read_default_file is handled by the mysql C libs
+    so we have to emulate the behaviour
+
+    Settings that are missing will return ''
+    returns (user, password, database_name, database_host, database_port)
+    """
+    read_default_file = dbinfo.get('OPTIONS', {}).get('read_default_file')
+    if read_default_file:
+        config = configparser.RawConfigParser({
+            'user': '',
+            'password': '',
+            'database': '',
+            'host': '',
+            'port': '',
+            'socket': '',
+        })
+        import os
+        config.read(os.path.expanduser(read_default_file))
+        try:
+            user = config.get('client', 'user')
+            password = config.get('client', 'password')
+            database_name = config.get('client', 'database')
+            database_host = config.get('client', 'host')
+            database_port = config.get('client', 'port')
+            socket = config.get('client', 'socket')
+
+            if database_host == 'localhost' and socket:
+                # mysql actually uses a socket if host is localhost
+                database_host = socket
+
+            return user, password, database_name, database_host, database_port
+
+        except configparser.NoSectionError:
+            pass
+
+    return '', '', '', '', ''


Test patch:
diff --git a/django_extensions/management/commands/drop_test_database.py b/django_extensions/management/commands/drop_test_database.py
--- a/django_extensions/management/commands/drop_test_database.py
+++ b/django_extensions/management/commands/drop_test_database.py
@@ -3,8 +3,9 @@
 
 from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
-from six.moves import configparser, input
+from six.moves import input
 
+from django_extensions.management.mysql import parse_mysql_cnf
 from django_extensions.management.utils import signalcommand
 
 try:
@@ -50,15 +51,9 @@ def handle(self, *args, **options):
 
         engine = dbinfo.get('ENGINE').split('.')[-1]
 
-        user = password = database_name = ''
+        user = password = database_name = database_host = database_port = ''
         if engine == 'mysql':
-            read_default_file = dbinfo.get('OPTIONS', {}).get('read_default_file')
-            if read_default_file:
-                config = configparser.ConfigParser()
-                config.read(read_default_file)
-                user = config.get('client', 'user')
-                password = config.get('client', 'password')
-                database_name = config.get('client', 'database')
+            (user, password, database_name, database_host, database_port) = parse_mysql_cnf(dbinfo)
 
         user = options.get('user') or dbinfo.get('USER') or user
         password = options.get('password') or dbinfo.get('PASSWORD') or password
@@ -74,8 +69,8 @@ def handle(self, *args, **options):
         if database_name is None or database_name == '':
             raise CommandError("You need to specify DATABASE_NAME in your Django settings file.")
 
-        database_host = dbinfo.get('HOST')
-        database_port = dbinfo.get('PORT')
+        database_host = dbinfo.get('HOST') or database_host
+        database_port = dbinfo.get('PORT') or database_port
 
         verbosity = int(options.get('verbosity', 1))
         if options.get('interactive'):