Ticket #1810: django-trunk-r2844-dsn.patch

File django-trunk-r2844-dsn.patch, 12.7 KB (added by Kumar McMillan <support-forums4@…>, 18 years ago)

patch for using DATABASE_DSN to set all database settings

  • django/conf/project_template/settings.py

     
    1515DATABASE_PASSWORD = ''         # Not used with sqlite3.
    1616DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
    1717DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
     18DATABASE_DSN = ''              # Overrides above settings.  Examples:
     19                               # 'postgres://joe:s3cr3t@localhost:8001/addressbook'
     20                               # 'sqlite3://:memory:'
     21                               # 'sqlite3:///path/to/addressbook.db'
    1822
    1923# Local time zone for this installation. All choices can be found here:
    2024# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
  • django/conf/__init__.py

     
    88
    99import os
    1010import sys
     11import urllib
    1112from django.conf import global_settings
    1213
    1314ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
     
    5556
    5657        # move the time zone info into os.environ
    5758        os.environ['TZ'] = self.TIME_ZONE
     59       
     60        # inject DSN attributes only if they were declared ...
     61        if getattr(self, 'DATABASE_DSN', None):
     62            for k,v in parse_dsn(self.DATABASE_DSN).items():
     63                if v is not None:
     64                    setattr(self, 'DATABASE_%s' % k.upper(), v)
    5865
     66def parse_dsn(dsn):
     67    """returns dict with keys: engine, name, user, password, host, port.
     68   
     69    The keys match the global setting names prefixed with DATABASE_.
     70    For example, ``name`` corresponds to ``DATABASE_NAME``.
     71    For any value not parsed from the dsn, its value is set to None.
     72   
     73    Since there is no real RFC on the DSN format (that I could find),
     74    The format here is based on the `URL RFC`_ and typical DSN handling.
     75   
     76    Some things to note ... 
     77    The URL RFC says the ``//`` part of ``scheme://path`` denotes an IP-accessible
     78    resource so to make things simpler for local engines, you can leave out
     79    the ``//`` or use ``/`` instead. 
     80    Thus, ``sqlite:/:memory:`` is valid, as is ``postgres:``.
     81    See the unit tests for a complete list of supported DSN formats.
     82   
     83    .. _URL RFC : http://www.freesoft.org/CIE/RFC/1738/index.htm
     84   
     85    @author Kumar McMillan <kumar dot mcmillan / gmail.com>
     86   
     87    """
     88    parsed = dict(engine=None, name=None, user=None, password=None, host=None, port=None)
     89   
     90    def parse_host(host):
     91        if host.find(':') != -1:
     92            parsed['host'], parsed['port'] = host.split(':')
     93        else:
     94            parsed['host'] = host
     95   
     96    def parse_name(name):
     97        if name.find('?') != -1:
     98            parsed['name'], query = urllib.splitquery(name)
     99            for attr in query.split('&'):
     100                k,v = urllib.splitvalue(attr)
     101                parsed[k] = urllib.unquote_plus(v)
     102        else:
     103            parsed['name'] = name
     104   
     105    def parse_user(user):
     106        if user.find(':') != -1:
     107            parsed['user'], parsed['password'] = user.split(':')
     108            parsed['password'] = urllib.unquote_plus(parsed['password'])
     109        else:
     110            parsed['user'] = user
     111        parsed['user'] = urllib.unquote_plus(parsed['user'])
     112   
     113    try:
     114        esep = dsn.find(':')
     115        if esep == -1:
     116            raise ValueError, 'no engine detected; expected "engine://"'
     117        parsed['engine'] = dsn[0:esep]
     118   
     119        post_scheme = dsn[esep+1:]
     120        # pop off the two optional slashes ...
     121        for n in range(2):
     122            if len(post_scheme) and post_scheme[0] == '/':
     123                post_scheme = post_scheme[1:]
     124       
     125        if post_scheme == '':
     126            return parsed
     127   
     128        if parsed['engine'].lower().startswith('sqlite'):
     129            # special case since we can't split on slashes :
     130            parsed['name'] = post_scheme
     131            return parsed
     132   
     133        parts = post_scheme.split('/')
     134        if len(parts) == 1:
     135            parse_name(parts[0])
     136            return parsed
     137   
     138        domain, name = parts
     139   
     140        if domain.find('@') != -1:
     141            user, host = domain.split('@')
     142            parse_user(user)
     143            parse_host(host)
     144            parse_name(name)
     145            return parsed
     146   
     147        parse_host(domain)
     148        parse_name(name)
     149        return parsed
     150    except:
     151        # append DSN to any exception raised ..
     152        etype, val, tb = sys.exc_info()
     153        val = "%s (dsn: '%s')" % (val, dsn)
     154        raise etype, val, tb
     155
    59156# try to load DJANGO_SETTINGS_MODULE
    60157try:
    61158    settings_module = os.environ[ENVIRONMENT_VARIABLE]
  • tests/othertests/conf.py

     
     1
     2"""
     3test the conf module ...
     4
     5"""
     6
     7from django.conf import Settings
     8from django.conf import parse_dsn
     9from django.conf.project_template import settings as default_settings
     10from django.conf import global_settings
     11import urllib
     12
     13def assert_eq(val1, val2):
     14    assert val1 == val2, "'%s' != '%s'" % (val1,val2)
     15
     16def test_parse_dsn():
     17    def assert_raises(fn, exc, args=tuple(), kw={}):
     18        try: fn(*args,**kw)
     19        except exc: pass
     20        else: raise AssertionError("exected call to %s to raise %s" % (fn,exc))
     21   
     22    assert_raises(parse_dsn, ValueError, ('',))
     23    assert_raises(parse_dsn, ValueError, ('postgres',))
     24   
     25    assert_eq( parse_dsn('postgres:'),
     26                dict(   engine='postgres', name=None, user=None,
     27                        password=None, host=None, port=None))
     28    assert_eq( parse_dsn('postgres:/'),
     29                dict(   engine='postgres', name=None, user=None,
     30                        password=None, host=None, port=None))
     31    assert_eq( parse_dsn('postgres://'),
     32                dict(   engine='postgres', name=None, user=None,
     33                        password=None, host=None, port=None))
     34    assert_eq( parse_dsn('PostGres://'),
     35                dict(   engine='PostGres', name=None, user=None,
     36                        password=None, host=None, port=None))
     37    assert_eq( parse_dsn('postgres://mydbname'),
     38                dict(   engine='postgres', name='mydbname', user=None,
     39                        password=None, host=None, port=None))
     40       
     41    assert_eq( parse_dsn('sqlite:/:memory:'),
     42                dict(   engine='sqlite', name=':memory:', user=None,
     43                        password=None, host=None, port=None))
     44    assert_eq( parse_dsn('sqlite://:memory:'),
     45                dict(   engine='sqlite', name=':memory:', user=None,
     46                        password=None, host=None, port=None))
     47    assert_eq( parse_dsn('sqlite:///path/to/db'),
     48                dict(   engine='sqlite', name='/path/to/db', user=None,
     49                        password=None, host=None, port=None))
     50    assert_eq( parse_dsn('sqlite3:///path/to/db'),
     51                dict(   engine='sqlite3', name='/path/to/db', user=None,
     52                        password=None, host=None, port=None))
     53    assert_eq( parse_dsn('sqlite://relpath/to/db'),
     54                dict(   engine='sqlite', name='relpath/to/db', user=None,
     55                        password=None, host=None, port=None))
     56   
     57    assert_eq( parse_dsn('mysql://myhost/dbname'),
     58                dict(   engine='mysql', name='dbname', user=None,
     59                        password=None, host='myhost', port=None))
     60    assert_eq( parse_dsn('mysql://me@myhost/dbname'),
     61                dict(   engine='mysql', name='dbname', user='me',
     62                        password=None, host='myhost', port=None))
     63    assert_eq( parse_dsn('mysql://me:mypass@myhost/dbname'),
     64                dict(   engine='mysql', name='dbname', user='me',
     65                        password='mypass', host='myhost', port=None))
     66    assert_eq( parse_dsn('mysql://me:mypass@myhost:9000/dbname'),
     67                dict(   engine='mysql', name='dbname', user='me',
     68                        password='mypass', host='myhost', port='9000'))
     69   
     70    # always decode ?? or do it lazily.  right now we are doing it always
     71    p = parse_dsn('postgres://%s:%s@localhost/mydbname' % \
     72                        (   urllib.quote_plus('wil/@:dman'),
     73                            urllib.quote_plus('//craazy%@passW')))
     74    assert_eq( p, dict( engine='postgres', name='mydbname', user='wil/@:dman',
     75                        password='//craazy%@passW', host='localhost', port=None))
     76    # check a literal password with percent sign ...
     77    assert_eq( parse_dsn('postgres://me:me%4pas@localhost/mydbname'),
     78                dict(   engine='postgres', name='mydbname', user='me',
     79                        password='me%4pas', host='localhost', port=None))
     80                       
     81    p = parse_dsn(  'mysql:///?host=localhost&name=dbname&'
     82                    'user=willy&password=pzzwd&port=8000')
     83    assert_eq( p, dict( engine='mysql', name='dbname', user='willy',
     84                        password='pzzwd', host='localhost', port='8000'))
     85    p = parse_dsn(  'mysql:///dbname?host=localhost&'
     86                    'user=willy&password=%s&port=8000' % urllib.quote('pa@swd'))
     87    assert_eq( p, dict( engine='mysql', name='dbname', user='willy',
     88                        password='pa@swd', host='localhost', port='8000'))
     89    # just a sanity check to make sure ? params override ...
     90    p = parse_dsn(  'mysql://noop:bad@otherhost:81/?host=localhost&name=db&'
     91                    'user=me&password=cheese&port=80')
     92    assert_eq( p, dict( engine='mysql', name='db', user='me',
     93                        password='cheese', host='localhost', port='80'))
     94   
     95def test_database_settings():
     96    global global_settings, default_settings
     97   
     98    # this is silly.  decouple Settings() a little more?
     99    saved_global_settings = global_settings
     100    saved_default_settings = default_settings
     101    try:
     102        settings_path = 'django.conf.project_template.settings'
     103   
     104        my_settings = default_settings
     105        my_settings.DATABASE_DSN = 'postgres://me:secr3t@supercool.com.fr:8001/postcards'
     106   
     107        s = Settings(settings_path)
     108        assert_eq( s.DATABASE_ENGINE, 'postgres')
     109        assert_eq( s.DATABASE_NAME, 'postcards')
     110        assert_eq( s.DATABASE_USER, 'me')
     111        assert_eq( s.DATABASE_PASSWORD, 'secr3t')
     112        assert_eq( s.DATABASE_HOST, 'supercool.com.fr')
     113        assert_eq( s.DATABASE_PORT, '8001')
     114   
     115        # we also want to only override what the dsn declares :
     116        my_settings.DATABASE_DSN = 'postgres://me@supercool.com/postcards'
     117        my_settings.DATABASE_USER = 'kumar'
     118        my_settings.DATABASE_PASSWORD = 'boggle'
     119   
     120        s = Settings(settings_path)
     121        assert_eq( s.DATABASE_USER, 'me')
     122        assert_eq( s.DATABASE_PASSWORD, 'boggle')
     123    finally:
     124        global_settings = saved_global_settings
     125        default_settings = saved_default_settings
     126
     127# hmm ..
     128test_parse_dsn()
     129test_database_settings()
     130 No newline at end of file
  • docs/settings.txt

     
    282282
    283283The username to use when connecting to the database. Not used with SQLite.
    284284
     285DATABASE_DSN
     286------------
     287
     288Default: ``''`` (Empty string)
     289
     290DSN (Data Source Name) string to set all configuration values at once.
     291For example, ``postgres://joe:s3cr3t@localhost:8001/addressbook`` will set the following :
     292
     293- ``DATABASE_ENGINE='postgres'``
     294- ``DATABASE_USER='joe'``
     295- ``DATABASE_PASSWORD='s3cr3t'``
     296- ``DATABASE_HOST='localhost'``
     297- ``DATABASE_PORT='8001'``
     298- ``DATABASE_NAME='addressbook'``
     299
     300Examples of other valid DSNs :
     301
     302- ``sqlite3://:memory:`` or ``sqlite3:/:memory:``
     303- ``sqlite3:///path/to/my/db``
     304- ``mysql:///?host=localhost&user=joe&password=s3cr3t&port=8001&name=addressbook``
     305- ``postgres:`` (i.e. everything blank, tell postgres to open a default connection for the current user)
     306
     307Any value retrieved from parsing a non-empty DSN string will replace a value
     308already set in its corresponding variable.  In other words, ``mysql://joe@localhost/joedb``
     309will replace the values (if any) of DATABASE_ENGINE, DATABASE_USER,
     310DATABASE_HOST and DATABASE_NAME.  However, it will **not** replace a value
     311in DATABASE_PORT.  Also note that *USER* and *PASSWORD* must be url encoded
     312if they contain characters like ``@``, ``:``, etc (see built-in ``urllib.quote_plus()``).
     313
    285314DATE_FORMAT
    286315-----------
    287316
Back to Top