7

Giving this dictionary:

>>> options = {'DATABASES': {'default': {'ENGINE': 'django.db.backends.sqlite3'}}}

What would be the best way to get this?:

>>> foo(options)
>>> print DATABASES
{'default': {'ENGINE': 'django.db.backends.sqlite3'}}

I am solving this as locals().update(options), but I was thinking, if there is maybe a better solution.

Mario César
  • 3,699
  • 2
  • 27
  • 42
  • 1
    The best way is not to do such dangerous things in the first place. And luckily, a function alone won't be able to fool around with the current namespace (actually it can, with the global one and perhaps with more if you use evil reflection hackery, but don't event think of it!) –  Jan 10 '11 at 19:32
  • @delnan I understand using locals() is ugly. Look, after parsing a config file I get a the options dictionary, so How can replace the values of the default values on the settings with out using locals() ¿ – Mario César Jan 10 '11 at 20:38
  • 1
    You better don't. Because doing so means overwriting your variables with user input, which is only one step above `eval`. Just go on with the dictionary. –  Jan 10 '11 at 21:04

4 Answers4

10
import inspect

allowed_vars = set(["min_", "max_", "path", ...])
def update_globals(dic):
    caller_frame = inspect.currentframe(1)
    globals = caller_frame.f_globals
    # here, you _could_ simply do globals.update(dic) 
    # but it is  evil
    for key, value in dic.items():
        #here you should carefully verify each key, and value for not
        #not dangerous pairs, with stuff like:
        #if key not in allowed_vars:
        #    sys.stderr.write("Warning: invalid variable in configuration update\n")
        #     continue
        #if type(value) not in (string, int, float):
        #    #(issue error)
        #    continue
        globals[key] = value

Example:

>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> update_globals({"a": 5})
>>> a
5

update 2016-06 A couple of weeks ago I had put together the extradict Python package - it is available on pypi now. One of its features is the MapGetter context manager that allows exactly what is being asked for by doing something along:

from extradict import MapGetter

def myfunc():
    options = {'DATABASES': {'default': {'ENGINE': 'django.db.backends.sqlite3'}}}
    with MapGetter(options) as options:
         from options import DATABASES
 ...

And other normal "from .... import .... " usages, but from a dictionary or mapping object (including a default dict).

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • I am aware that modifying locals or globals it's something dangerous, but your method seems to work as I need it. Probably in the future I would change the behavior of my parser for configuration – Mario César Jan 12 '11 at 13:40
  • 1
    It is not modifying locals or globals that is dangerous per se - that is part of Python's dynamism and we have to make use of it. What is dangerous is picking data from an unverified source (normally remote users input), and convert that into "living" python objects in the running program. Provided that you add the safeguards I tip at above, tehre should be no problem using this code. – jsbueno Jan 12 '11 at 16:09
  • @jsbueno I found `extradict` to be an elegant library, and also perfect for this problem. Thank you! – Jérémie Jun 22 '20 at 04:43
  • @Jérémie: thank you. I resumed some work on my personal projects over the last week. "Extradict" is also benefiting from that. Refactoring "MapGetter" to properly use the import machinery, instead of the current hack, is on the roadmap, but its usage will stay the same. – jsbueno Jun 22 '20 at 13:11
3

I think what you want is simply:

globals().update(**options)
vinyll
  • 11,017
  • 2
  • 48
  • 37
3

As goldmab noted, modifying the output of locals() inside a function won't work:

SyntaxError: invalid syntax
>>> def foo():
...     locals().update({'a': 1})
...     print a
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
NameError: global name 'a' is not defined

This isn't really a very clean way either, but it would do the job:

>>> def foo():
...     options = {'DATABASES': {'default': {'ENGINE': 'django.db.backends.sqlite3'}}}
...     for k, v in options.items():
...         exec('%s = v' % k)
...     print DATABASES
... 
>>> foo()
{'default': {'ENGINE': 'django.db.backends.sqlite3'}}

Note, by the way, that each key in your dict would need to be ok as a variable. So for example, if the dictionary contained 'DATABASE-USERNAME' as a key, trying to assign that to a variable would cause an exception. Further, doing this would make you vulnerable to code injection attacks if you get the options dictionary from an untrustworthy source. (A key could say something like, "import os ; os.system('sudo adduser scriptkiddie') ; ..."

Ken Kinder
  • 12,654
  • 6
  • 50
  • 70
0

You can't modify function locals at runtime, since the list of local variable names is a static part of the compiled function object.

>>> def a(): b = 5
... 
>>> a.func_code.co_varnames
('b',)

This will work in the global scope only because locals() is the same as globals() there, and global variables are stored in a dictionary, which is dynamic (unlike function locals).

>>> locals() is globals()
True

It looks like you're updating the values in a Django settings module from another source. I wouldn't say this is necessarily bad, but you should use globals() instead of locals() for the sake of clarity.

Brian Goldman
  • 716
  • 3
  • 11