diff --git a/examples/pylonstest/nose.ini b/examples/pylonstest/nose.ini new file mode 100644 index 00000000..599f3e27 --- /dev/null +++ b/examples/pylonstest/nose.ini @@ -0,0 +1,65 @@ +# +# pylonstest - Pylons testing environment configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = false +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:gunicorn#main +host = 192.168.0.1 +port = 80 +workers = 2 +proc_name = brim +ignore_this = True + +[app:main] +use = egg:pylonstest +full_stack = true +static_files = true + +cache_dir = %(here)s/data +beaker.session.key = pylonstest +beaker.session.secret = somesecret + +# Add additional test specific configuration options as necessary. +# Logging configuration +[loggers] +keys = root, routes, pylonstest + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_routes] +level = INFO +handlers = +qualname = routes.middleware +# "level = DEBUG" logs the route matched and routing variables. + +[logger_pylonstest] +level = DEBUG +handlers = +qualname = pylonstest + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + diff --git a/gunicorn/app/base.py b/gunicorn/app/base.py index 34070baf..201b4d0f 100644 --- a/gunicorn/app/base.py +++ b/gunicorn/app/base.py @@ -33,7 +33,13 @@ class Application(object): # Load up the config file if its found. if opts.config and os.path.exists(opts.config): - cfg = globals().copy() + cfg = { + "__builtins__": __builtins__, + "__name__": "__config__", + "__file__": opts.config, + "__doc__": None, + "__package__": None + } try: execfile(opts.config, cfg, cfg) except Exception, e: @@ -42,6 +48,9 @@ class Application(object): sys.exit(1) for k, v in list(cfg.items()): + # Ignore unknown names + if k not in self.cfg.settings: + continue self.cfg.set(k.lower(), v) # Lastly, update the configuration with any command line diff --git a/gunicorn/app/pasterapp.py b/gunicorn/app/pasterapp.py index 8fce6845..066d16a0 100644 --- a/gunicorn/app/pasterapp.py +++ b/gunicorn/app/pasterapp.py @@ -47,7 +47,12 @@ class PasterApplication(Application): cfg['workers'] = int(lc.get('workers', 1)) cfg['umask'] = int(lc.get('umask', 0)) cfg['default_proc_name'] = gc.get('__file__') - cfg.update(dict((k,v) for (k,v) in lc.items() if k not in cfg)) + + for k, v in lc.items(): + if k not in self.cfg.settings: + continue + cfg[k] = v + return cfg def load(self): diff --git a/gunicorn/config.py b/gunicorn/config.py index 8702bef1..2e8dc961 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -5,7 +5,7 @@ from __future__ import with_statement -import ConfigParser +import copy import grp import inspect import optparse @@ -23,7 +23,7 @@ KNOWN_SETTINGS = [] class Config(object): def __init__(self, usage=None): - self.settings = dict((s.name, s) for s in KNOWN_SETTINGS) + self.settings = dict((s.name, s.copy()) for s in KNOWN_SETTINGS) self.usage = usage def __getattr__(self, name): @@ -78,7 +78,6 @@ class Config(object): @property def uid(self): user = self.settings['user'].get() - if not user: return os.geteuid() elif user.isdigit() or isinstance(user, int): @@ -89,7 +88,6 @@ class Config(object): @property def gid(self): group = self.settings['group'].get() - if not group: return os.getegid() elif group.isdigit() or isinstance(user, int): @@ -105,18 +103,6 @@ class Config(object): else: return self.settings['default_proc_name'] - @property - def pre_fork(self): - return self.settings['pre_fork'].get() - - @property - def post_fork(self): - return self.settings['post_fork'].get() - - @property - def pre_exec(self): - return self.settings['pre_exec'].get() - class Setting(object): def __init__(self, name): self.name = name @@ -162,6 +148,9 @@ class Setting(object): kwargs.pop("type") parser.add_option(*args, **kwargs) + def copy(self): + return copy.copy(self) + def get(self): return self.value @@ -172,6 +161,8 @@ class Setting(object): def validate_bool(val): if isinstance(val, types.BooleanType): return val + if not isinstance(val, basestring): + raise TypeError("Invalid type for casting: %s" % val) if val.lower().strip() == "true": return True elif val.lower().strip() == "false": @@ -182,11 +173,17 @@ def validate_bool(val): def validate_pos_int(val): if not isinstance(val, (types.IntType, types.LongType)): val = int(val, 0) + else: + # Booleans are ints! + val = int(val) + print "Setting: %s" % val if val < 0: raise ValueError("Value must be positive: %s" % val) return val def validate_string(val): + if not isinstance(val, basestring): + raise TypeError("Not a string: %s" % val) return val.strip() def validate_callable(arity): @@ -203,7 +200,7 @@ with Setting("config") as s: s.cli = ["-c", "--config"] s.meta = "FILE" s.validator = validate_string - s.default = "gunicorn.conf.py" + s.default = None s.fmt_desc("""\ The path to a Gunicorn config file. diff --git a/gunicorn/http/tee.py b/gunicorn/http/tee.py index 385051d1..aaf90cea 100644 --- a/gunicorn/http/tee.py +++ b/gunicorn/http/tee.py @@ -39,7 +39,7 @@ class TeeInput(object): elif self._len and self._len < util.MAX_BODY: self.tmp = StringIO() else: - self.tmp = tempfile.TemporaryFile(dir=self.cfg['tmp_upload_dir']) + self.tmp = tempfile.TemporaryFile(dir=self.cfg.tmp_upload_dir) if len(buf.getvalue()) > 0: chunk, self.buf = parser.filter_body(buf) diff --git a/tests/001-test-parser.py b/tests/001-test-parser.py index ca9e3217..e324a07c 100644 --- a/tests/001-test-parser.py +++ b/tests/001-test-parser.py @@ -167,7 +167,7 @@ def test_010(buf, p): body = "" while not p.body_eof(): chunk, buf2 = p.filter_body(buf2) - print chunk + #print chunk if chunk: body += chunk t.eq(body, "all your base are belong to us") diff --git a/tests/003-test-config.py b/tests/003-test-config.py new file mode 100644 index 00000000..4d888e10 --- /dev/null +++ b/tests/003-test-config.py @@ -0,0 +1,186 @@ + +from nose.plugins.skip import SkipTest + +import t + +import functools +import os +import sys + +from gunicorn import config +from gunicorn.app.base import Application +from gunicorn.workers.sync import SyncWorker + +dirname = os.path.dirname(__file__) +def cfg_file(): + return os.path.join(dirname, "config", "test_cfg.py") +def paster_ini(): + return os.path.join(dirname, "..", "examples", "pylonstest", "nose.ini") + +def PasterApp(): + try: + from paste.deploy import loadapp, loadwsgi + except ImportError: + raise SkipTest() + from gunicorn.app.pasterapp import PasterApplication + return PasterApplication("no_usage") + +class AltArgs(object): + def __init__(self, args=None): + self.args = args or [] + self.orig = sys.argv + + def __enter__(self): + sys.argv = self.args + + def __exit__(self, exc_type, exc_inst, traceback): + sys.argv = self.orig + +class NoConfigApp(Application): + def __init__(self): + super(NoConfigApp, self).__init__("no_usage") + + def init(self, parser, opts, args): + pass + + def load(self): + pass + + +def test_defaults(): + c = config.Config() + for s in config.KNOWN_SETTINGS: + t.eq(s.default, c.settings[s.name].get()) + +def test_property_access(): + c = config.Config() + for s in config.KNOWN_SETTINGS: + getattr(c, s.name) + + # Class was loaded + t.eq(c.worker_class, SyncWorker) + + # Debug affects workers + t.eq(c.workers, 1) + c.set("workers", 3) + t.eq(c.workers, 3) + c.set("debug", True) + t.eq(c.workers, 1) + + # Address is parsed + t.eq(c.address, ("127.0.0.1", 8000)) + + # User and group defaults + t.eq(os.geteuid(), c.uid) + t.eq(os.getegid(), c.gid) + + # Proc name + t.eq("gunicorn", c.proc_name) + + # Not a config property + t.raises(AttributeError, getattr, c, "foo") + # Force to be not an error + class Baz(object): + def get(self): + return 3.14 + c.settings["foo"] = Baz() + t.eq(c.foo, 3.14) + + # Attempt to set a cfg not via c.set + t.raises(AttributeError, setattr, c, "proc_name", "baz") + + # No setting for name + t.raises(AttributeError, c.set, "baz", "bar") + +def test_bool_validation(): + c = config.Config() + t.eq(c.debug, False) + c.set("debug", True) + t.eq(c.debug, True) + c.set("debug", "true") + t.eq(c.debug, True) + c.set("debug", "false") + t.eq(c.debug, False) + t.raises(ValueError, c.set, "debug", "zilch") + t.raises(TypeError, c.set, "debug", 4) + +def test_pos_int_validation(): + c = config.Config() + t.eq(c.workers, 1) + c.set("workers", 4) + t.eq(c.workers, 4) + c.set("workers", "5") + t.eq(c.workers, 5) + c.set("workers", "0xFF") + t.eq(c.workers, 255) + c.set("workers", True) + t.eq(c.workers, 1) # Yes. That's right... + t.raises(ValueError, c.set, "workers", -21) + t.raises(TypeError, c.set, "workers", c) + +def test_str_validation(): + c = config.Config() + t.eq(c.proc_name, "gunicorn") + c.set("proc_name", " foo ") + t.eq(c.proc_name, "foo") + t.raises(TypeError, c.set, "proc_name", 2) + +def test_callable_validation(): + c = config.Config() + def func(a, b): + pass + c.set("pre_fork", func) + t.eq(c.pre_fork, func) + t.raises(TypeError, c.set, "pre_fork", 1) + t.raises(TypeError, c.set, "pre_fork", lambda x: True) + +def test_cmd_line(): + with AltArgs(["prog_name", "-b", "blargh"]): + app = NoConfigApp() + t.eq(app.cfg.bind, "blargh") + with AltArgs(["prog_name", "-w", "3"]): + app = NoConfigApp() + t.eq(app.cfg.workers, 3) + with AltArgs(["prog_name", "-d"]): + app = NoConfigApp() + t.eq(app.cfg.debug, True) + +def test_app_config(): + with AltArgs(): + app = NoConfigApp() + for s in config.KNOWN_SETTINGS: + t.eq(s.default, app.cfg.settings[s.name].get()) + +def test_load_config(): + with AltArgs(["prog_name", "-c", cfg_file()]): + app = NoConfigApp() + t.eq(app.cfg.bind, "unix:/tmp/bar/baz") + t.eq(app.cfg.workers, 3) + t.eq(app.cfg.proc_name, "fooey") + +def test_cli_overrides_config(): + with AltArgs(["prog_name", "-c", cfg_file(), "-b", "blarney"]): + app = NoConfigApp() + t.eq(app.cfg.bind, "blarney") + t.eq(app.cfg.proc_name, "fooey") + +def test_paster_config(): + with AltArgs(["prog_name", paster_ini()]): + app = PasterApp() + t.eq(app.cfg.bind, "192.168.0.1:80") + t.eq(app.cfg.proc_name, "brim") + t.eq("ignore_me" in app.cfg.settings, False) + +def test_cfg_over_paster(): + with AltArgs(["prog_name", "-c", cfg_file(), paster_ini()]): + app = PasterApp() + t.eq(app.cfg.bind, "unix:/tmp/bar/baz") + t.eq(app.cfg.proc_name, "fooey") + t.eq(app.cfg.default_proc_name, "blurgh") + +def test_cli_cfg_paster(): + with AltArgs(["prog_name", "-c", cfg_file(), "-b", "whee", paster_ini()]): + app = PasterApp() + t.eq(app.cfg.bind, "whee") + t.eq(app.cfg.proc_name, "fooey") + t.eq(app.cfg.default_proc_name, "blurgh") diff --git a/tests/config/test_cfg.py b/tests/config/test_cfg.py new file mode 100644 index 00000000..70b8074b --- /dev/null +++ b/tests/config/test_cfg.py @@ -0,0 +1,4 @@ +bind = "unix:/tmp/bar/baz" +workers = 3 +proc_name = "fooey" +default_proc_name = "blurgh" \ No newline at end of file diff --git a/tests/t.py b/tests/t.py index 1aacd0c6..639e4698 100644 --- a/tests/t.py +++ b/tests/t.py @@ -75,7 +75,7 @@ class http_request(object): def __call__(self, func): def run(): fsock = FakeSocket(data_source(self.fname)) - req = Request(fsock, ('127.0.0.1', 6000), ('127.0.0.1', 8000), Config({})) + req = Request(Config(), fsock, ('127.0.0.1', 6000), ('127.0.0.1', 8000)) func(req) run.func_name = func.func_name return run