From cf7e560f89016a302342821f5071941a09c04283 Mon Sep 17 00:00:00 2001 From: hao huang Date: Sat, 28 Mar 2015 08:10:39 +0000 Subject: Fix typo in docs/index.txt --- .hgignore | 13 + MANIFEST.in | 2 + README | 11 + docs/_static/paste.css | 15 + docs/_templates/layout.html | 20 + docs/conf.py | 132 ++++ docs/index.txt | 672 +++++++++++++++++++ docs/license.txt | 20 + docs/modules/config.txt | 13 + docs/modules/converters.txt | 11 + docs/modules/loadwsgi.txt | 13 + docs/news.txt | 179 +++++ paste/__init__.py | 18 + paste/deploy/__init__.py | 3 + paste/deploy/compat.py | 32 + paste/deploy/config.py | 305 +++++++++ paste/deploy/converters.py | 40 ++ paste/deploy/loadwsgi.py | 725 +++++++++++++++++++++ paste/deploy/paster_templates.py | 36 + .../paste_deploy/+package+/sampleapp.py_tmpl | 23 + .../paste_deploy/+package+/wsgiapp.py_tmpl | 24 + .../paste_deploy/docs/devel_config.ini_tmpl | 22 + paste/deploy/util.py | 73 +++ regen-docs | 9 + setup.cfg | 2 + setup.py | 59 ++ tests/__init__.py | 13 + .../FakeApp.egg/FakeApp.egg-info/PKG-INFO | 10 + .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 22 + .../FakeApp.egg/FakeApp.egg-info/top_level.txt | 1 + .../fake_packages/FakeApp.egg/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 69 ++ .../FakeApp.egg/fakeapp/configapps.py | 14 + tests/fake_packages/FakeApp.egg/setup.py | 23 + tests/fixture.py | 20 + tests/sample_configs/basic_app.ini | 14 + tests/sample_configs/executable.ini | 10 + tests/sample_configs/test_config.ini | 38 ++ tests/sample_configs/test_config_included.ini | 10 + tests/sample_configs/test_error.ini | 8 + tests/sample_configs/test_filter.ini | 22 + tests/sample_configs/test_filter_with.ini | 12 + tests/sample_configs/test_func.ini | 13 + tests/test_basic_app.py | 36 + tests/test_config.py | 173 +++++ tests/test_config_middleware.py | 28 + tests/test_converters.py | 17 + tests/test_filter.py | 53 ++ tests/test_load_package.py | 12 + tox.ini | 14 + 50 files changed, 3105 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 docs/_static/paste.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/conf.py create mode 100644 docs/index.txt create mode 100644 docs/license.txt create mode 100644 docs/modules/config.txt create mode 100644 docs/modules/converters.txt create mode 100644 docs/modules/loadwsgi.txt create mode 100644 docs/news.txt create mode 100644 paste/__init__.py create mode 100644 paste/deploy/__init__.py create mode 100644 paste/deploy/compat.py create mode 100644 paste/deploy/config.py create mode 100644 paste/deploy/converters.py create mode 100644 paste/deploy/loadwsgi.py create mode 100644 paste/deploy/paster_templates.py create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl create mode 100644 paste/deploy/util.py create mode 100755 regen-docs create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/apps.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/configapps.py create mode 100644 tests/fake_packages/FakeApp.egg/setup.py create mode 100644 tests/fixture.py create mode 100644 tests/sample_configs/basic_app.ini create mode 100755 tests/sample_configs/executable.ini create mode 100644 tests/sample_configs/test_config.ini create mode 100644 tests/sample_configs/test_config_included.ini create mode 100644 tests/sample_configs/test_error.ini create mode 100644 tests/sample_configs/test_filter.ini create mode 100644 tests/sample_configs/test_filter_with.ini create mode 100644 tests/sample_configs/test_func.ini create mode 100644 tests/test_basic_app.py create mode 100644 tests/test_config.py create mode 100644 tests/test_config_middleware.py create mode 100644 tests/test_converters.py create mode 100644 tests/test_filter.py create mode 100644 tests/test_load_package.py create mode 100644 tox.ini diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..44abbb2 --- /dev/null +++ b/.hgignore @@ -0,0 +1,13 @@ +syntax: glob +*.egg-info/ +*.egg/ +*.pyc +*.class +dist/ +build/ +docs/_build/ +.tox +.project +.pydevproject +.settings +__pycache__ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7a2ffb9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include docs/*.txt +recursive-include paste/deploy/paster_templates * diff --git a/README b/README new file mode 100644 index 0000000..291a026 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). + +For the latest changes see the `news file +`_. \ No newline at end of file diff --git a/docs/_static/paste.css b/docs/_static/paste.css new file mode 100644 index 0000000..6705e5d --- /dev/null +++ b/docs/_static/paste.css @@ -0,0 +1,15 @@ +a.invisible-link { + color: #fff; + text-decoration: none; +} + +a.invisible-link:visited { + color: #fff; + text-decoration: none; +} + +a.invisible:link { + color: #fff; + text-decoration: none; +} + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..a6d2a97 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,20 @@ +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block sidebartoc %} +

Python Paste

+ + + +{{ super() }} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..af16cab --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Paste Deploy' +copyright = '2011, Ian Bicking and contributors' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '1.5' +# The full version, including alpha/beta/rc tags. +release = '1.5.2' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = ['include/contact.txt', 'include/reference_header.txt'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PasteDeploydoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..c085d27 --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,672 @@ +Paste Deployment +================ + +:author: Ian Bicking + +.. contents:: + +Documents: + +.. toctree:: + :maxdepth: 1 + + news + modules/loadwsgi + modules/config + modules/converters + license + +.. comment: + The names used in sections should be more concrete, and it should + be clearer that they are just arbitrary names. + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +To see updates that have been made to Paste Deploy see the `news file +`_. + +Paste Deploy is released under the `MIT license +`_. + +Status +------ + +Paste Deploy has passed version 1.0. Paste Deploy is an actively +maintained project. As of 1.0, we'll make a strong effort to maintain +backward compatibility (this actually started happening long before +1.0, but now it is explicit). This will include deprecation warnings +when necessary. Major changes will take place under new functions or +with new entry points. + +Note that the most key aspect of Paste Deploy is the entry points it +defines (such as ``paste.app_factory``). Paste Deploy is not the only +consumer of these entry points, and many extensions can best take +place by utilizing the entry points instead of using Paste Deploy +directly. The entry points will not change; if changes are necessary, +new entry points will be defined. + +Installation +------------ + +First make sure you have either +`setuptools `_ or its +modern replacement +`distribute `_ installed. +For Python 3.x you need distribute as setuptools does not work on it. + +Then you can install Paste Deployment using `pip +`_ by running:: + + $ sudo pip install PasteDeploy + +If you want to track development, do:: + + $ hg clone http://bitbucket.org/ianb/pastedeploy + $ cd pastedeploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. You can also simply install ``PasteDeploy==dev``. + +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + +A complimentary package is `Paste Script `_. To install +that, use ``pip install PasteScript`` (or ``pip install +PasteScript==dev``). + +From the User Perspective +------------------------- + +In the following sections, the Python API for using Paste Deploy is +given. This isn't what users will be using (but it is useful for +Python developers and useful for setting up tests fixtures). + +The primary interaction with Paste Deploy is through its configuration +files. The primary thing you want to do with a configuration file is +serve it. To learn about serving configuration files, see `the +``paster serve`` command +`_. + +The Config File +~~~~~~~~~~~~~~~ + +A config file has different sections. The only sections Paste Deploy +cares about have prefixes, like ``app:main`` or ``filter:errors`` -- +the part after the ``:`` is the "name" of the section, and the part +before gives the "type". Other sections are ignored. + +The format is a simple `INI format +`_: ``name = value``. You can +extend the value by indenting subsequent lines. ``#`` is a comment. + +Typically you have one or two sections, named "main": an application +section (``[app:main]``) and a server section (``[server:main]``). +``[composite:...]`` signifies something that dispatches to multiple +applications (example below). + +Here's a typical configuration file that also shows off mounting +multiple applications using `paste.urlmap +`_:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /wiki = wiki + /cms = config:cms.ini + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +I'll explain each section in detail now:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + +That this is a ``composite`` section means it dispatches the request +to other applications. ``use = egg:Paste#urlmap`` means to use the +composite application named ``urlmap`` from the ``Paste`` package. +``urlmap`` is a particularly common composite application -- it uses a +path prefix to map your request to another application. These are +the applications like "home", "blog", "wiki" and "config:cms.ini". The last +one just refers to another file ``cms.ini`` in the same directory. + +Next up:: + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + +``egg:Paste#static`` is another simple application, in this case it +just serves up non-dynamic files. It takes one bit of configuration: +``document_root``. You can use variable substitution, which will pull +variables from the section ``[DEFAULT]`` (case sensitive!) with +markers like ``%(var_name)s``. The special variable ``%(here)s`` is +the directory containing the configuration file; you should use that +in lieu of relative filenames (which depend on the current directory, +which can change depending how the server is run). + +Then:: + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +The ``[filter-app:blog]`` section means that you want an application +with a filter applied. The application being filtered is indicated +with ``next`` (which refers to the next section). The +``egg:Authentication#auth`` filter doesn't actually exist, but one +could imagine it logs people in and checks permissions. + +That last section is just a reference to an application that you +probably installed with ``pip install BlogApp``, and one bit of +configuration you passed to it (``database``). + +Lastly:: + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +This section is similar to the previous one, with one important difference. +Instead of an entry point in an egg, it refers directly to the ``application`` +variable in the ``mywiki.main`` module. The reference consist of two parts, +separated by a colon. The left part is the full name of the module and the +right part is the path to the variable, as a Python expression relative to the +containing module. + +So, that's most of the features you'll use. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or a callable from a module: + [app:mythirdapp] + use = call:my.project:myapplication + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composite Applications +~~~~~~~~~~~~~~~~~~~~~~ + +"Composite" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composite:main] + use = egg:Paste#urlmap + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composite application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +Filter Composition +~~~~~~~~~~~~~~~~~~ + +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + # and you could have another filter-with here, and so on... + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + + [filter:filter1] + ... + +Getting Configuration +~~~~~~~~~~~~~~~~~~~~~ + +If you want to get the configuration without creating the application, +you can use the ``appconfig(uri)`` function, which is just like the +``loadapp()`` function except it returns the configuration that would +be used, as a dictionary. Both global and local configuration is +combined into a single dictionary, but you can look at just one or the +other with the attributes ``.local_conf`` and ``.global_conf``. + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ and +`distribute `_ that adds metadata to a +normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory``, +``paste.composite_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composite_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composites are just slightly more complex:: + + def composite_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composite factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composite:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.filter_app_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + +``paste.server_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +The implementation of ``Server`` is left to the user. + +``paste.server_runner`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + +Outstanding Issues +------------------ + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory`` and ``paste.composite_factory`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + diff --git a/docs/license.txt b/docs/license.txt new file mode 100644 index 0000000..c810dec --- /dev/null +++ b/docs/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2006-2007 Ian Bicking and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/modules/config.txt b/docs/modules/config.txt new file mode 100644 index 0000000..9d8f894 --- /dev/null +++ b/docs/modules/config.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.config` -- Configuration and Environment middleware +====================================================================== + +.. automodule:: paste.deploy.config + +Module Contents +--------------- + +.. autoclass:: DispatchingConfig +.. autoclass:: ConfigMiddleware +.. autoclass:: PrefixMiddleware + +.. comment: FIXME: do something about CONFIG (manual docs?) diff --git a/docs/modules/converters.txt b/docs/modules/converters.txt new file mode 100644 index 0000000..4948104 --- /dev/null +++ b/docs/modules/converters.txt @@ -0,0 +1,11 @@ +:mod:`paste.deploy.converters` -- Conversion helpers for String Configuration +============================================================================= + +.. automodule:: paste.deploy.converters + +Module Contents +--------------- + +.. autofunction:: asbool +.. autofunction:: asint +.. autofunction:: aslist diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt new file mode 100644 index 0000000..feebf28 --- /dev/null +++ b/docs/modules/loadwsgi.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.loadwsgi` -- Load WSGI applications from config files +======================================================================== + +.. automodule:: paste.deploy.loadwsgi + +Module Contents +--------------- + +.. autofunction:: loadapp +.. autofunction:: loadserver +.. autofunction:: loadfilter +.. autofunction:: appconfig + diff --git a/docs/news.txt b/docs/news.txt new file mode 100644 index 0000000..bc1e987 --- /dev/null +++ b/docs/news.txt @@ -0,0 +1,179 @@ +Paste Deployment News +===================== + +1.5.2 +----- + +* Fixed Python 3 issue in paste.deploy.util.fix_type_error() + +1.5.1 +----- + +* Fixed use of the wrong variable when determining the context protocol + +* Fixed invalid import of paste.deploy.Config to paste.deploy.config.Config + +* Fixed multi proxy IPs bug in X-Forwarded-For header in PrefixMiddleware + +* Fixed TypeError when trying to raise LookupError on Python 3 + +* Fixed exception reraise on Python 3 + +Thanks to Alexandre Conrad, Atsushi Odagiri, Pior Bastida and Tres Seaver for their contributions. + +1.5.0 +----- + +* Project is now maintained by Alex Grönholm + +* Was printing extraneous data when calling setup.py + +* Fixed missing paster template files (fixes "paster create -t paste.deploy") + +* Excluded tests from release distributions + +* Added support for the "call:" protocol for loading apps directly as + functions (contributed by Jason Stitt) + +* Added Python 3.x support + +* Dropped Python 2.4 support + +* Removed the ``paste.deploy.epdesc`` and ``paste.deploy.interfaces`` modules + -- contact the maintainer if you actually needed them + +1.3.4 +----- + +* Fix loadconfig path handling on Jython on Windows. + +1.3.3 +----- + +* In :class:`paste.deploy.config.PrefixMiddleware` the headers + ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated + to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` + is translated to ``environ['REMOTE_ADDR']`` + +* Also in PrefixMiddleware, if X-Forwarded-Host has multiple + (comma-separated) values, use only the first value. + +1.3.2 +----- + +* Added ``paste.deploy.converters.asint()``. +* fixed use sections overwriting the config's __file__ value with the + use'd filename. +* ``paste.deploy.loadwsgi`` now supports variable expansion in the + DEFAULT section of config files (unlike plain ConfigParser). + +1.3.1 +----- + +* Fix ``appconfig`` config loading when using a config file with + ``filter-with`` in it (previously you'd get TypeError: iteration + over non-sequence) + +1.3 +--- + +* Added ``scheme`` option to ``PrefixMiddleware``, so you can force a + scheme (E.g., when proxying an HTTPS connection over HTTP). + +* Pop proper values into ``environ['paste.config']`` in + ``ConfigMiddleware``. + +1.1 +--- + +* Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other + loaders) will show up as though they were in ``[DEFAULT]``, so they + can be used in variable interpolation. Note: this won't overwrite + any existing values in ``[DEFAULT]``. + +* Added ``force_port`` option to + ``paste.deploy.config.PrefixMiddleware``. Also the ``prefix`` + argument is stripped of any trailing ``/``, which can't be valid in + that position. + +1.0 +--- + +* Added some documentation for the different kinds of entry points + Paste Deploy uses. + +* Added a feature to ``PrefixMiddleware`` that translates the + ``X-Forwarded-Server`` header to ``Host``. + +0.9.6 +----- + +* Added ``PrefixMiddleware`` which compensates for cases where the + wsgi app is behind a proxy of some sort that isn't moving the prefix + into the SCRIPT_NAME in advance. + +* Changed _loadconfig() so that it works with Windows absolute paths. + +* Make the error messages prettier when you call a function and fail + to give an argument, like a required function argument. + +0.5 +--- + +* Made the ``paste_deploy`` template (used with ``paster create + --template=paste_deploy``) more useful, with an example application + and entry point. + +0.4 +--- + +* Allow filters to have ``filter-with`` values, just like + applications. + +* Renamed ``composit`` to ``composite`` (old names still work, but + aren't documented). + +* Added ``appconfig()`` to load along with ``loadapp()``, but return + the configuration without invoking the application. + +0.3 +--- + +* Allow variable setting like:: + + get local_var = global_var_name + + To bring in global variables to the local scope. + +* Allow interpolation in files, like ``%(here)s``. Anything in the + ``[DEFAULTS]`` section will be available to substitute into a value, + as will variables in the same section. Also, the special value + ``here`` will be the directory the configuration file is located in. + +0.2 +--- + +Released 26 August 2004 + +* Added a ``filter-with`` setting to applications. + +* Removed the ``1`` from all the protocol names (e.g., + ``paste.app_factory1`` is not ``paste.app_factory``). + +* Added ``filter-app:`` and ``pipeline:`` sections. `Docs + `__. + +* Added ``paste.filter_app_factory1`` (`doc + `__) and + ``paste.server_runner1`` (`doc + `__) protocols. + +* Added ``paste.deploy.converters`` module for handling the + string values that are common with this system. + +0.1 +--- + +Released 22 August 2004 + +Initial version released. It's all new. diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..cdb6121 --- /dev/null +++ b/paste/__init__.py @@ -0,0 +1,18 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + # don't prevent use of paste if pkg_resources isn't installed + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) + +try: + import modulefinder +except ImportError: + pass +else: + for p in __path__: + modulefinder.AddPackagePath(__name__, p) + diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py new file mode 100644 index 0000000..94c63a8 --- /dev/null +++ b/paste/deploy/__init__.py @@ -0,0 +1,3 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +from paste.deploy.loadwsgi import * diff --git a/paste/deploy/compat.py b/paste/deploy/compat.py new file mode 100644 index 0000000..05047db --- /dev/null +++ b/paste/deploy/compat.py @@ -0,0 +1,32 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +"""Python 2<->3 compatibility module""" +import sys + + +def print_(template, *args, **kwargs): + template = str(template) + if args: + template = template % args + elif kwargs: + template = template % kwargs + sys.stdout.writelines(template) + +if sys.version_info < (3, 0): + basestring = basestring + from ConfigParser import ConfigParser + from urllib import unquote + iteritems = lambda d: d.iteritems() + dictkeys = lambda d: d.keys() + + def reraise(t, e, tb): + exec('raise t, e, tb', dict(t=t, e=e, tb=tb)) +else: + basestring = str + from configparser import ConfigParser + from urllib.parse import unquote + iteritems = lambda d: d.items() + dictkeys = lambda d: list(d.keys()) + + def reraise(t, e, tb): + raise e.with_traceback(tb) diff --git a/paste/deploy/config.py b/paste/deploy/config.py new file mode 100644 index 0000000..a503007 --- /dev/null +++ b/paste/deploy/config.py @@ -0,0 +1,305 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +"""Paste Configuration Middleware and Objects""" +import threading +import re + +# Loaded lazily +wsgilib = None +local = None + +__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware', 'PrefixMiddleware'] + + +def local_dict(): + global config_local, local + try: + return config_local.wsgi_dict + except NameError: + config_local = threading.local() + config_local.wsgi_dict = result = {} + return result + except AttributeError: + config_local.wsgi_dict = result = {} + return result + + +class DispatchingConfig(object): + + """ + This is a configuration object that can be used globally, + imported, have references held onto. The configuration may differ + by thread (or may not). + + Specific configurations are registered (and deregistered) either + for the process or for threads. + """ + + # @@: What should happen when someone tries to add this + # configuration to itself? Probably the conf should become + # resolved, and get rid of this delegation wrapper + + _constructor_lock = threading.Lock() + + def __init__(self): + self._constructor_lock.acquire() + try: + self.dispatching_id = 0 + while 1: + self._local_key = 'paste.processconfig_%i' % self.dispatching_id + if not self._local_key in local_dict(): + break + self.dispatching_id += 1 + finally: + self._constructor_lock.release() + self._process_configs = [] + + def push_thread_config(self, conf): + """ + Make ``conf`` the active configuration for this thread. + Thread-local configuration always overrides process-wide + configuration. + + This should be used like:: + + conf = make_conf() + dispatching_config.push_thread_config(conf) + try: + ... do stuff ... + finally: + dispatching_config.pop_thread_config(conf) + """ + local_dict().setdefault(self._local_key, []).append(conf) + + def pop_thread_config(self, conf=None): + """ + Remove a thread-local configuration. If ``conf`` is given, + it is checked against the popped configuration and an error + is emitted if they don't match. + """ + self._pop_from(local_dict()[self._local_key], conf) + + def _pop_from(self, lst, conf): + popped = lst.pop() + if conf is not None and popped is not conf: + raise AssertionError( + "The config popped (%s) is not the same as the config " + "expected (%s)" + % (popped, conf)) + + def push_process_config(self, conf): + """ + Like push_thread_config, but applies the configuration to + the entire process. + """ + self._process_configs.append(conf) + + def pop_process_config(self, conf=None): + self._pop_from(self._process_configs, conf) + + def __getattr__(self, attr): + conf = self.current_conf() + if conf is None: + raise AttributeError( + "No configuration has been registered for this process " + "or thread") + return getattr(conf, attr) + + def current_conf(self): + thread_configs = local_dict().get(self._local_key) + if thread_configs: + return thread_configs[-1] + elif self._process_configs: + return self._process_configs[-1] + else: + return None + + def __getitem__(self, key): + # I thought __getattr__ would catch this, but apparently not + conf = self.current_conf() + if conf is None: + raise TypeError( + "No configuration has been registered for this process " + "or thread") + return conf[key] + + def __contains__(self, key): + # I thought __getattr__ would catch this, but apparently not + return key in self + + def __setitem__(self, key, value): + # I thought __getattr__ would catch this, but apparently not + conf = self.current_conf() + conf[key] = value + +CONFIG = DispatchingConfig() + + +class ConfigMiddleware(object): + + """ + A WSGI middleware that adds a ``paste.config`` key to the request + environment, as well as registering the configuration temporarily + (for the length of the request) with ``paste.CONFIG``. + """ + + def __init__(self, application, config): + """ + This delegates all requests to `application`, adding a *copy* + of the configuration `config`. + """ + self.application = application + self.config = config + + def __call__(self, environ, start_response): + global wsgilib + if wsgilib is None: + import pkg_resources + pkg_resources.require('Paste') + from paste import wsgilib + popped_config = None + if 'paste.config' in environ: + popped_config = environ['paste.config'] + conf = environ['paste.config'] = self.config.copy() + app_iter = None + CONFIG.push_thread_config(conf) + try: + app_iter = self.application(environ, start_response) + finally: + if app_iter is None: + # An error occurred... + CONFIG.pop_thread_config(conf) + if popped_config is not None: + environ['paste.config'] = popped_config + if type(app_iter) in (list, tuple): + # Because it is a concrete iterator (not a generator) we + # know the configuration for this thread is no longer + # needed: + CONFIG.pop_thread_config(conf) + if popped_config is not None: + environ['paste.config'] = popped_config + return app_iter + else: + def close_config(): + CONFIG.pop_thread_config(conf) + new_app_iter = wsgilib.add_close(app_iter, close_config) + return new_app_iter + + +def make_config_filter(app, global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return ConfigMiddleware(app, conf) + +make_config_middleware = ConfigMiddleware.__doc__ + + +class PrefixMiddleware(object): + """Translate a given prefix into a SCRIPT_NAME for the filtered + application. + + PrefixMiddleware provides a way to manually override the root prefix + (SCRIPT_NAME) of your application for certain, rare situations. + + When running an application under a prefix (such as '/james') in + FastCGI/apache, the SCRIPT_NAME environment variable is automatically + set to to the appropriate value: '/james'. Pylons' URL generating + functions, such as url_for, always take the SCRIPT_NAME value into account. + + One situation where PrefixMiddleware is required is when an application + is accessed via a reverse proxy with a prefix. The application is accessed + through the reverse proxy via the the URL prefix '/james', whereas the + reverse proxy forwards those requests to the application at the prefix '/'. + + The reverse proxy, being an entirely separate web server, has no way of + specifying the SCRIPT_NAME variable; it must be manually set by a + PrefixMiddleware instance. Without setting SCRIPT_NAME, url_for will + generate URLs such as: '/purchase_orders/1', when it should be + generating: '/james/purchase_orders/1'. + + To filter your application through a PrefixMiddleware instance, add the + following to the '[app:main]' section of your .ini file: + + .. code-block:: ini + + filter-with = proxy-prefix + + [filter:proxy-prefix] + use = egg:PasteDeploy#prefix + prefix = /james + + The name ``proxy-prefix`` simply acts as an identifier of the filter + section; feel free to rename it. + + Also, unless disabled, the ``X-Forwarded-Server`` header will be + translated to the ``Host`` header, for cases when that header is + lost in the proxying. Also ``X-Forwarded-Host``, + ``X-Forwarded-Scheme``, and ``X-Forwarded-Proto`` are translated. + + If ``force_port`` is set, SERVER_PORT and HTTP_HOST will be + rewritten with the given port. You can use a number, string (like + '80') or the empty string (whatever is the default port for the + scheme). This is useful in situations where there is port + forwarding going on, and the server believes itself to be on a + different port than what the outside world sees. + + You can also use ``scheme`` to explicitly set the scheme (like + ``scheme = https``). + """ + def __init__(self, app, global_conf=None, prefix='/', + translate_forwarded_server=True, + force_port=None, scheme=None): + self.app = app + self.prefix = prefix.rstrip('/') + self.translate_forwarded_server = translate_forwarded_server + self.regprefix = re.compile("^%s(.*)$" % self.prefix) + self.force_port = force_port + self.scheme = scheme + + def __call__(self, environ, start_response): + url = environ['PATH_INFO'] + url = re.sub(self.regprefix, r'\1', url) + if not url: + url = '/' + environ['PATH_INFO'] = url + environ['SCRIPT_NAME'] = self.prefix + if self.translate_forwarded_server: + if 'HTTP_X_FORWARDED_SERVER' in environ: + environ['SERVER_NAME'] = environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER').split(',')[0] + if 'HTTP_X_FORWARDED_HOST' in environ: + environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST').split(',')[0] + if 'HTTP_X_FORWARDED_FOR' in environ: + environ['REMOTE_ADDR'] = environ.pop('HTTP_X_FORWARDED_FOR').split(',')[0] + if 'HTTP_X_FORWARDED_SCHEME' in environ: + environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_SCHEME') + elif 'HTTP_X_FORWARDED_PROTO' in environ: + environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_PROTO') + if self.force_port is not None: + host = environ.get('HTTP_HOST', '').split(':', 1)[0] + if self.force_port: + host = '%s:%s' % (host, self.force_port) + environ['SERVER_PORT'] = str(self.force_port) + else: + if environ['wsgi.url_scheme'] == 'http': + port = '80' + else: + port = '443' + environ['SERVER_PORT'] = port + environ['HTTP_HOST'] = host + if self.scheme is not None: + environ['wsgi.url_scheme'] = self.scheme + return self.app(environ, start_response) + + +def make_prefix_middleware( + app, global_conf, prefix='/', + translate_forwarded_server=True, + force_port=None, scheme=None): + from paste.deploy.converters import asbool + translate_forwarded_server = asbool(translate_forwarded_server) + return PrefixMiddleware( + app, prefix=prefix, + translate_forwarded_server=translate_forwarded_server, + force_port=force_port, scheme=scheme) + +make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py new file mode 100644 index 0000000..c9d87de --- /dev/null +++ b/paste/deploy/converters.py @@ -0,0 +1,40 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +from paste.deploy.compat import basestring + + +truthy = frozenset(['true', 'yes', 'on', 'y', 't', '1']) +falsy = frozenset(['false', 'no', 'off', 'n', 'f', '0']) + + +def asbool(obj): + if isinstance(obj, basestring): + obj = obj.strip().lower() + if obj in truthy: + return True + elif obj in falsy: + return False + else: + raise ValueError("String is not true/false: %r" % obj) + return bool(obj) + + +def asint(obj): + try: + return int(obj) + except (TypeError, ValueError): + raise ValueError("Bad integer value: %r" % obj) + + +def aslist(obj, sep=None, strip=True): + if isinstance(obj, basestring): + lst = obj.split(sep) + if strip: + lst = [v.strip() for v in lst] + return lst + elif isinstance(obj, (list, tuple)): + return obj + elif obj is None: + return [] + else: + return [obj] diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py new file mode 100644 index 0000000..8b2849d --- /dev/null +++ b/paste/deploy/loadwsgi.py @@ -0,0 +1,725 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +from __future__ import with_statement +import os +import sys +import re + +import pkg_resources + +from paste.deploy.compat import ConfigParser, unquote, iteritems, dictkeys +from paste.deploy.util import fix_call, lookup_object + +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + + +############################################################ +## Utility functions +############################################################ + + +def import_string(s): + return pkg_resources.EntryPoint.parse("x=" + s).load(False) + + +def _aslist(obj): + """ + Turn object into a list; lists and tuples are left as-is, None + becomes [], and everything else turns into a one-element list. + """ + if obj is None: + return [] + elif isinstance(obj, (list, tuple)): + return obj + else: + return [obj] + + +def _flatten(lst): + """ + Flatten a nested list. + """ + if not isinstance(lst, (list, tuple)): + return [lst] + result = [] + for item in lst: + result.extend(_flatten(item)) + return result + + +class NicerConfigParser(ConfigParser): + + def __init__(self, filename, *args, **kw): + ConfigParser.__init__(self, *args, **kw) + self.filename = filename + if hasattr(self, '_interpolation'): + self._interpolation = self.InterpolateWrapper(self._interpolation) + + read_file = getattr(ConfigParser, 'read_file', ConfigParser.readfp) + + def defaults(self): + """Return the defaults, with their values interpolated (with the + defaults dict itself) + + Mainly to support defaults using values such as %(here)s + """ + defaults = ConfigParser.defaults(self).copy() + for key, val in iteritems(defaults): + defaults[key] = self.get('DEFAULT', key) or val + return defaults + + def _interpolate(self, section, option, rawval, vars): + # Python < 3.2 + try: + return ConfigParser._interpolate( + self, section, option, rawval, vars) + except Exception: + e = sys.exc_info()[1] + args = list(e.args) + args[0] = 'Error in file %s: %s' % (self.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + class InterpolateWrapper(object): + # Python >= 3.2 + def __init__(self, original): + self._original = original + + def __getattr__(self, name): + return getattr(self._original, name) + + def before_get(self, parser, section, option, value, defaults): + try: + return self._original.before_get(parser, section, option, + value, defaults) + except Exception: + e = sys.exc_info()[1] + args = list(e.args) + args[0] = 'Error in file %s: %s' % (parser.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + +############################################################ +## Object types +############################################################ + + +class _ObjectType(object): + + name = None + egg_protocols = None + config_prefixes = None + + def __init__(self): + # Normalize these variables: + self.egg_protocols = [_aslist(p) for p in _aslist(self.egg_protocols)] + self.config_prefixes = [_aslist(p) for p in _aslist(self.config_prefixes)] + + def __repr__(self): + return '<%s protocols=%r prefixes=%r>' % ( + self.name, self.egg_protocols, self.config_prefixes) + + def invoke(self, context): + assert context.protocol in _flatten(self.egg_protocols) + return fix_call(context.object, + context.global_conf, **context.local_conf) + + +class _App(_ObjectType): + + name = 'application' + egg_protocols = ['paste.app_factory', 'paste.composite_factory', + 'paste.composit_factory'] + config_prefixes = [['app', 'application'], ['composite', 'composit'], + 'pipeline', 'filter-app'] + + def invoke(self, context): + if context.protocol in ('paste.composit_factory', + 'paste.composite_factory'): + return fix_call(context.object, + context.loader, context.global_conf, + **context.local_conf) + elif context.protocol == 'paste.app_factory': + return fix_call(context.object, context.global_conf, **context.local_conf) + else: + assert 0, "Protocol %r unknown" % context.protocol + +APP = _App() + + +class _Filter(_ObjectType): + name = 'filter' + egg_protocols = [['paste.filter_factory', 'paste.filter_app_factory']] + config_prefixes = ['filter'] + + def invoke(self, context): + if context.protocol == 'paste.filter_factory': + return fix_call(context.object, + context.global_conf, **context.local_conf) + elif context.protocol == 'paste.filter_app_factory': + def filter_wrapper(wsgi_app): + # This should be an object, so it has a nicer __repr__ + return fix_call(context.object, + wsgi_app, context.global_conf, + **context.local_conf) + return filter_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol + +FILTER = _Filter() + + +class _Server(_ObjectType): + name = 'server' + egg_protocols = [['paste.server_factory', 'paste.server_runner']] + config_prefixes = ['server'] + + def invoke(self, context): + if context.protocol == 'paste.server_factory': + return fix_call(context.object, + context.global_conf, **context.local_conf) + elif context.protocol == 'paste.server_runner': + def server_wrapper(wsgi_app): + # This should be an object, so it has a nicer __repr__ + return fix_call(context.object, + wsgi_app, context.global_conf, + **context.local_conf) + return server_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol + +SERVER = _Server() + + +# Virtual type: (@@: There's clearly something crufty here; +# this probably could be more elegant) +class _PipeLine(_ObjectType): + name = 'pipeline' + + def invoke(self, context): + app = context.app_context.create() + filters = [c.create() for c in context.filter_contexts] + filters.reverse() + for filter in filters: + app = filter(app) + return app + +PIPELINE = _PipeLine() + + +class _FilterApp(_ObjectType): + name = 'filter_app' + + def invoke(self, context): + next_app = context.next_context.create() + filter = context.filter_context.create() + return filter(next_app) + +FILTER_APP = _FilterApp() + + +class _FilterWith(_App): + name = 'filtered_with' + + def invoke(self, context): + filter = context.filter_context.create() + filtered = context.next_context.create() + if context.next_context.object_type is APP: + return filter(filtered) + else: + # filtering a filter + def composed(app): + return filter(filtered(app)) + return composed + +FILTER_WITH = _FilterWith() + + +############################################################ +## Loaders +############################################################ + + +def loadapp(uri, name=None, **kw): + return loadobj(APP, uri, name=name, **kw) + + +def loadfilter(uri, name=None, **kw): + return loadobj(FILTER, uri, name=name, **kw) + + +def loadserver(uri, name=None, **kw): + return loadobj(SERVER, uri, name=name, **kw) + + +def appconfig(uri, name=None, relative_to=None, global_conf=None): + context = loadcontext(APP, uri, name=name, + relative_to=relative_to, + global_conf=global_conf) + return context.config() + +_loaders = {} + + +def loadobj(object_type, uri, name=None, relative_to=None, + global_conf=None): + context = loadcontext( + object_type, uri, name=name, relative_to=relative_to, + global_conf=global_conf) + return context.create() + + +def loadcontext(object_type, uri, name=None, relative_to=None, + global_conf=None): + if '#' in uri: + if name is None: + uri, name = uri.split('#', 1) + else: + # @@: Ignore fragment or error? + uri = uri.split('#', 1)[0] + if name is None: + name = 'main' + if ':' not in uri: + raise LookupError("URI has no scheme: %r" % uri) + scheme, path = uri.split(':', 1) + scheme = scheme.lower() + if scheme not in _loaders: + raise LookupError( + "URI scheme not known: %r (from %s)" + % (scheme, ', '.join(_loaders.keys()))) + return _loaders[scheme]( + object_type, + uri, path, name=name, relative_to=relative_to, + global_conf=global_conf) + + +def _loadconfig(object_type, uri, path, name, relative_to, + global_conf): + isabs = os.path.isabs(path) + # De-Windowsify the paths: + path = path.replace('\\', '/') + if not isabs: + if not relative_to: + raise ValueError( + "Cannot resolve relative uri %r; no relative_to keyword " + "argument given" % uri) + relative_to = relative_to.replace('\\', '/') + if relative_to.endswith('/'): + path = relative_to + path + else: + path = relative_to + '/' + path + if path.startswith('///'): + path = path[2:] + path = unquote(path) + loader = ConfigLoader(path) + if global_conf: + loader.update_defaults(global_conf, overwrite=False) + return loader.get_context(object_type, name, global_conf) + +_loaders['config'] = _loadconfig + + +def _loadegg(object_type, uri, spec, name, relative_to, + global_conf): + loader = EggLoader(spec) + return loader.get_context(object_type, name, global_conf) + +_loaders['egg'] = _loadegg + + +def _loadfunc(object_type, uri, spec, name, relative_to, + global_conf): + + loader = FuncLoader(spec) + return loader.get_context(object_type, name, global_conf) + +_loaders['call'] = _loadfunc + +############################################################ +## Loaders +############################################################ + + +class _Loader(object): + + def get_app(self, name=None, global_conf=None): + return self.app_context( + name=name, global_conf=global_conf).create() + + def get_filter(self, name=None, global_conf=None): + return self.filter_context( + name=name, global_conf=global_conf).create() + + def get_server(self, name=None, global_conf=None): + return self.server_context( + name=name, global_conf=global_conf).create() + + def app_context(self, name=None, global_conf=None): + return self.get_context( + APP, name=name, global_conf=global_conf) + + def filter_context(self, name=None, global_conf=None): + return self.get_context( + FILTER, name=name, global_conf=global_conf) + + def server_context(self, name=None, global_conf=None): + return self.get_context( + SERVER, name=name, global_conf=global_conf) + + _absolute_re = re.compile(r'^[a-zA-Z]+:') + + def absolute_name(self, name): + """ + Returns true if the name includes a scheme + """ + if name is None: + return False + return self._absolute_re.search(name) + + +class ConfigLoader(_Loader): + + def __init__(self, filename): + self.filename = filename = filename.strip() + defaults = { + 'here': os.path.dirname(os.path.abspath(filename)), + '__file__': os.path.abspath(filename) + } + self.parser = NicerConfigParser(filename, defaults=defaults) + self.parser.optionxform = str # Don't lower-case keys + with open(filename) as f: + self.parser.read_file(f) + + def update_defaults(self, new_defaults, overwrite=True): + for key, value in iteritems(new_defaults): + if not overwrite and key in self.parser._defaults: + continue + self.parser._defaults[key] = value + + def get_context(self, object_type, name=None, global_conf=None): + if self.absolute_name(name): + return loadcontext(object_type, name, + relative_to=os.path.dirname(self.filename), + global_conf=global_conf) + section = self.find_config_section( + object_type, name=name) + if global_conf is None: + global_conf = {} + else: + global_conf = global_conf.copy() + defaults = self.parser.defaults() + global_conf.update(defaults) + local_conf = {} + global_additions = {} + get_from_globals = {} + for option in self.parser.options(section): + if option.startswith('set '): + name = option[4:].strip() + global_additions[name] = global_conf[name] = ( + self.parser.get(section, option)) + elif option.startswith('get '): + name = option[4:].strip() + get_from_globals[name] = self.parser.get(section, option) + else: + if option in defaults: + # @@: It's a global option (?), so skip it + continue + local_conf[option] = self.parser.get(section, option) + for local_var, glob_var in get_from_globals.items(): + local_conf[local_var] = global_conf[glob_var] + if object_type in (APP, FILTER) and 'filter-with' in local_conf: + filter_with = local_conf.pop('filter-with') + else: + filter_with = None + if 'require' in local_conf: + for spec in local_conf['require'].split(): + pkg_resources.require(spec) + del local_conf['require'] + if section.startswith('filter-app:'): + context = self._filter_app_context( + object_type, section, name=name, + global_conf=global_conf, local_conf=local_conf, + global_additions=global_additions) + elif section.startswith('pipeline:'): + context = self._pipeline_app_context( + object_type, section, name=name, + global_conf=global_conf, local_conf=local_conf, + global_additions=global_additions) + elif 'use' in local_conf: + context = self._context_from_use( + object_type, local_conf, global_conf, global_additions, + section) + else: + context = self._context_from_explicit( + object_type, local_conf, global_conf, global_additions, + section) + if filter_with is not None: + filter_with_context = LoaderContext( + obj=None, + object_type=FILTER_WITH, + protocol=None, + global_conf=global_conf, local_conf=local_conf, + loader=self) + filter_with_context.filter_context = self.filter_context( + name=filter_with, global_conf=global_conf) + filter_with_context.next_context = context + return filter_with_context + return context + + def _context_from_use(self, object_type, local_conf, global_conf, + global_additions, section): + use = local_conf.pop('use') + context = self.get_context( + object_type, name=use, global_conf=global_conf) + context.global_conf.update(global_additions) + context.local_conf.update(local_conf) + if '__file__' in global_conf: + # use sections shouldn't overwrite the original __file__ + context.global_conf['__file__'] = global_conf['__file__'] + # @@: Should loader be overwritten? + context.loader = self + + if context.protocol is None: + # Determine protocol from section type + section_protocol = section.split(':', 1)[0] + if section_protocol in ('application', 'app'): + context.protocol = 'paste.app_factory' + elif section_protocol in ('composit', 'composite'): + context.protocol = 'paste.composit_factory' + else: + # This will work with 'server' and 'filter', otherwise it + # could fail but there is an error message already for + # bad protocols + context.protocol = 'paste.%s_factory' % section_protocol + + return context + + def _context_from_explicit(self, object_type, local_conf, global_conf, + global_addition, section): + possible = [] + for protocol_options in object_type.egg_protocols: + for protocol in protocol_options: + if protocol in local_conf: + possible.append((protocol, local_conf[protocol])) + break + if len(possible) > 1: + raise LookupError( + "Multiple protocols given in section %r: %s" + % (section, possible)) + if not possible: + raise LookupError( + "No loader given in section %r" % section) + found_protocol, found_expr = possible[0] + del local_conf[found_protocol] + value = import_string(found_expr) + context = LoaderContext( + value, object_type, found_protocol, + global_conf, local_conf, self) + return context + + def _filter_app_context(self, object_type, section, name, + global_conf, local_conf, global_additions): + if 'next' not in local_conf: + raise LookupError( + "The [%s] section in %s is missing a 'next' setting" + % (section, self.filename)) + next_name = local_conf.pop('next') + context = LoaderContext(None, FILTER_APP, None, global_conf, + local_conf, self) + context.next_context = self.get_context( + APP, next_name, global_conf) + if 'use' in local_conf: + context.filter_context = self._context_from_use( + FILTER, local_conf, global_conf, global_additions, + section) + else: + context.filter_context = self._context_from_explicit( + FILTER, local_conf, global_conf, global_additions, + section) + return context + + def _pipeline_app_context(self, object_type, section, name, + global_conf, local_conf, global_additions): + if 'pipeline' not in local_conf: + raise LookupError( + "The [%s] section in %s is missing a 'pipeline' setting" + % (section, self.filename)) + pipeline = local_conf.pop('pipeline').split() + if local_conf: + raise LookupError( + "The [%s] pipeline section in %s has extra " + "(disallowed) settings: %s" + % (', '.join(local_conf.keys()))) + context = LoaderContext(None, PIPELINE, None, global_conf, + local_conf, self) + context.app_context = self.get_context( + APP, pipeline[-1], global_conf) + context.filter_contexts = [ + self.get_context(FILTER, name, global_conf) + for name in pipeline[:-1]] + return context + + def find_config_section(self, object_type, name=None): + """ + Return the section name with the given name prefix (following the + same pattern as ``protocol_desc`` in ``config``. It must have the + given name, or for ``'main'`` an empty name is allowed. The + prefix must be followed by a ``:``. + + Case is *not* ignored. + """ + possible = [] + for name_options in object_type.config_prefixes: + for name_prefix in name_options: + found = self._find_sections( + self.parser.sections(), name_prefix, name) + if found: + possible.extend(found) + break + if not possible: + raise LookupError( + "No section %r (prefixed by %s) found in config %s" + % (name, + ' or '.join(map(repr, _flatten(object_type.config_prefixes))), + self.filename)) + if len(possible) > 1: + raise LookupError( + "Ambiguous section names %r for section %r (prefixed by %s) " + "found in config %s" + % (possible, name, + ' or '.join(map(repr, _flatten(object_type.config_prefixes))), + self.filename)) + return possible[0] + + def _find_sections(self, sections, name_prefix, name): + found = [] + if name is None: + if name_prefix in sections: + found.append(name_prefix) + name = 'main' + for section in sections: + if section.startswith(name_prefix + ':'): + if section[len(name_prefix) + 1:].strip() == name: + found.append(section) + return found + + +class EggLoader(_Loader): + + def __init__(self, spec): + self.spec = spec + + def get_context(self, object_type, name=None, global_conf=None): + if self.absolute_name(name): + return loadcontext(object_type, name, + global_conf=global_conf) + entry_point, protocol, ep_name = self.find_egg_entry_point( + object_type, name=name) + return LoaderContext( + entry_point, + object_type, + protocol, + global_conf or {}, {}, + self, + distribution=pkg_resources.get_distribution(self.spec), + entry_point_name=ep_name) + + def find_egg_entry_point(self, object_type, name=None): + """ + Returns the (entry_point, protocol) for the with the given + ``name``. + """ + if name is None: + name = 'main' + possible = [] + for protocol_options in object_type.egg_protocols: + for protocol in protocol_options: + pkg_resources.require(self.spec) + entry = pkg_resources.get_entry_info( + self.spec, + protocol, + name) + if entry is not None: + possible.append((entry.load(), protocol, entry.name)) + break + if not possible: + # Better exception + dist = pkg_resources.get_distribution(self.spec) + raise LookupError( + "Entry point %r not found in egg %r (dir: %s; protocols: %s; " + "entry_points: %s)" + % (name, self.spec, + dist.location, + ', '.join(_flatten(object_type.egg_protocols)), + ', '.join(_flatten([ + dictkeys(pkg_resources.get_entry_info(self.spec, prot, name) or {}) + for prot in protocol_options] or '(no entry points)')))) + if len(possible) > 1: + raise LookupError( + "Ambiguous entry points for %r in egg %r (protocols: %s)" + % (name, self.spec, ', '.join(_flatten(protocol_options)))) + return possible[0] + + +class FuncLoader(_Loader): + """ Loader that supports specifying functions inside modules, without + using eggs at all. Configuration should be in the format: + use = call:my.module.path:function_name + + Dot notation is supported in both the module and function name, e.g.: + use = call:my.module.path:object.method + """ + def __init__(self, spec): + self.spec = spec + if not ':' in spec: + raise LookupError("Configuration not in format module:function") + + def get_context(self, object_type, name=None, global_conf=None): + obj = lookup_object(self.spec) + return LoaderContext( + obj, + object_type, + None, # determine protocol from section type + global_conf or {}, + {}, + self, + ) + + +class LoaderContext(object): + + def __init__(self, obj, object_type, protocol, + global_conf, local_conf, loader, + distribution=None, entry_point_name=None): + self.object = obj + self.object_type = object_type + self.protocol = protocol + #assert protocol in _flatten(object_type.egg_protocols), ( + # "Bad protocol %r; should be one of %s" + # % (protocol, ', '.join(map(repr, _flatten(object_type.egg_protocols))))) + self.global_conf = global_conf + self.local_conf = local_conf + self.loader = loader + self.distribution = distribution + self.entry_point_name = entry_point_name + + def create(self): + return self.object_type.invoke(self) + + def config(self): + conf = AttrDict(self.global_conf) + conf.update(self.local_conf) + conf.local_conf = self.local_conf + conf.global_conf = self.global_conf + conf.context = self + return conf + + +class AttrDict(dict): + """ + A dictionary that can be assigned to. + """ + pass diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py new file mode 100644 index 0000000..9c5f942 --- /dev/null +++ b/paste/deploy/paster_templates.py @@ -0,0 +1,36 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +import os + +from paste.script.templates import Template + +from paste.deploy.compat import print_ + + +class PasteDeploy(Template): + + _template_dir = 'paster_templates/paste_deploy' + summary = "A web application deployed through paste.deploy" + + egg_plugins = ['PasteDeploy'] + + required_templates = ['PasteScript#basic_package'] + + def post(self, command, output_dir, vars): + for prereq in ['PasteDeploy']: + command.insert_into_file( + os.path.join(output_dir, 'setup.py'), + 'Extra requirements', + '%r,\n' % prereq, + indent=True) + command.insert_into_file( + os.path.join(output_dir, 'setup.py'), + 'Entry points', + (' [paste.app_factory]\n' + ' main = %(package)s.wsgiapp:make_app\n') % vars, + indent=False) + if command.verbose: + print_('*' * 72) + print_('* Run "paster serve docs/devel_config.ini" to run the sample application') + print_('* on http://localhost:8080') + print_('*' * 72) diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl new file mode 100644 index 0000000..cb49352 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -0,0 +1,23 @@ +import cgi + +from paste.deploy.config import CONFIG + + +def application(environ, start_response): + # Note that usually you wouldn't be writing a pure WSGI + # application, you might be using some framework or + # environment. But as an example... + start_response('200 OK', [('Content-type', 'text/html')]) + greeting = CONFIG['greeting'] + content = [ + '%s\n' % greeting, + '

%s!

\n' % greeting, + '\n', + ] + items = environ.items() + items.sort() + for key, value in items: + content.append('\n' + % (key, cgi.escape(repr(value)))) + content.append('
%s%s
') + return content diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl new file mode 100644 index 0000000..97decb8 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,24 @@ +from paste.deploy.config import ConfigMiddleware + +import sampleapp + + +def make_app( + global_conf, + # Optional and required configuration parameters + # can go here, or just **kw; greeting is required: + greeting, + **kw): + # This is a WSGI application: + app = sampleapp.application + # Here we merge all the keys into one configuration + # dictionary; you don't have to do this, but this + # can be convenient later to add ad hoc configuration: + conf = global_conf.copy() + conf.update(kw) + conf['greeting'] = greeting + # ConfigMiddleware means that paste.deploy.CONFIG will, + # during this request (threadsafe) represent the + # configuration dictionary we set up: + app = ConfigMiddleware(app, conf) + return app diff --git a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl new file mode 100644 index 0000000..0c0ae35 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -0,0 +1,22 @@ +[filter-app:main] +# This puts the interactive debugger in place: +use = egg:Paste#evalerror +next = devel + +[app:devel] +# This application is meant for interactive development +use = egg:${project} +debug = true +# You can add other configuration values: +greeting = Aloha! + +[app:test] +# While this version of the configuration is for non-iteractive +# tests (unit tests) +use = devel + +[server:main] +use = egg:Paste#http +# Change to 0.0.0.0 to make public: +host = 127.0.0.1 +port = 8080 diff --git a/paste/deploy/util.py b/paste/deploy/util.py new file mode 100644 index 0000000..b6f766a --- /dev/null +++ b/paste/deploy/util.py @@ -0,0 +1,73 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +import inspect +import sys + +from paste.deploy.compat import reraise + + +def fix_type_error(exc_info, callable, varargs, kwargs): + """ + Given an exception, this will test if the exception was due to a + signature error, and annotate the error with better information if + so. + + Usage:: + + try: + val = callable(*args, **kw) + except TypeError: + exc_info = fix_type_error(None, callable, args, kw) + raise exc_info[0], exc_info[1], exc_info[2] + """ + if exc_info is None: + exc_info = sys.exc_info() + if (exc_info[0] != TypeError + or str(exc_info[1]).find('arguments') == -1 + or getattr(exc_info[1], '_type_error_fixed', False)): + return exc_info + exc_info[1]._type_error_fixed = True + argspec = inspect.formatargspec(*inspect.getargspec(callable)) + args = ', '.join(map(_short_repr, varargs)) + if kwargs and args: + args += ', ' + if kwargs: + kwargs = sorted(kwargs.items()) + args += ', '.join(['%s=...' % n for n, v in kwargs]) + gotspec = '(%s)' % args + msg = '%s; got %s, wanted %s' % (exc_info[1], gotspec, argspec) + exc_info[1].args = (msg,) + return exc_info + + +def _short_repr(v): + v = repr(v) + if len(v) > 12: + v = v[:8] + '...' + v[-4:] + return v + + +def fix_call(callable, *args, **kw): + """ + Call ``callable(*args, **kw)`` fixing any type errors that come out. + """ + try: + val = callable(*args, **kw) + except TypeError: + exc_info = fix_type_error(None, callable, args, kw) + reraise(*exc_info) + return val + + +def lookup_object(spec): + """ + Looks up a module or object from a some.module:func_name specification. + To just look up a module, omit the colon and everything after it. + """ + parts, target = spec.split(':') if ':' in spec else (spec, None) + module = __import__(parts) + + for part in parts.split('.')[1:] + ([target] if target else []): + module = getattr(module, part) + + return module diff --git a/regen-docs b/regen-docs new file mode 100755 index 0000000..f8dad75 --- /dev/null +++ b/regen-docs @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + scp -r _build/* ianb@webwareforpython.org:/home/paste/htdocs/deploy/ +fi diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f15c017 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..79f9d70 --- /dev/null +++ b/setup.py @@ -0,0 +1,59 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.dirname(__file__) +readme_path = os.path.join(here, 'README') +readme = open(readme_path).read() + + +setup( + name='PasteDeploy', + version='1.5.2', + description='Load, configure, and compose WSGI applications and servers', + long_description=readme, + classifiers=[ + 'Development Status :: 6 - Mature', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Paste', + ], + keywords='web wsgi application server', + author='Ian Bicking', + author_email='ianb@colorstudy.com', + maintainer='Alex Gronholm', + maintainer_email='alex.gronholm@nextday.fi', + url='http://pythonpaste.org/deploy/', + license='MIT', + namespace_packages=['paste'], + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + test_suite='nose.collector', + tests_require=['nose>=0.11'], + extras_require={ + 'Config': [], + 'Paste': ['Paste'], + }, + entry_points=""" + [paste.filter_app_factory] + config = paste.deploy.config:make_config_filter [Config] + prefix = paste.deploy.config:make_prefix_middleware + + [paste.paster_create_template] + paste_deploy=paste.deploy.paster_templates:PasteDeploy + """ +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..cffe526 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +import os +import sys + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +sys.path.insert(0, base) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +pkg_resources.require('PasteDeploy') diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt new file mode 100644 index 0000000..9bfc986 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -0,0 +1,22 @@ +[paste.app_factory] + + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + + +[paste.composit_factory] + + remote_addr=fakeapp.apps:make_remote_addr + + +[paste.filter_app_factory] + + caps2=fakeapp.apps:CapFilter + + +[paste.filter_factory] + + caps=fakeapp.apps:make_cap_filter + + diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py new file mode 100644 index 0000000..cae7eba --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -0,0 +1,69 @@ +############################################################ +## Apps +############################################################ + +def simple_app(response, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['This is ', response] + +def basic_app(environ, start_response): + return simple_app('basic app', environ, start_response) + +def make_basic_app(global_conf, **conf): + return basic_app + +def basic_app2(environ, start_response): + return simple_app('basic app2', environ, start_response) + +def make_basic_app2(global_conf, **conf): + return basic_app2 + +############################################################ +## Composits +############################################################ + +def make_remote_addr(loader, global_conf, **conf): + apps = {} + addrs = {} + for name, value in conf.items(): + if name.startswith('app.'): + apps[name[4:]] = loader.get_app(value, global_conf) + elif name.startswith('addr.'): + addrs[name[5:]] = value + dispatcher = RemoteAddrDispatch() + for name in apps: + dispatcher.map[addrs[name]] = apps[name] + return dispatcher + +class RemoteAddrDispatch(object): + def __init__(self, map=None): + self.map = map or {} + + def __call__(self, environ, start_response): + addr = environ['REMOTE_ADDR'] + app = self.map.get(addr) or self.map['0.0.0.0'] + return app(environ, start_response) + +############################################################ +## Filters +############################################################ + +def make_cap_filter(global_conf, method_to_call='upper'): + def cap_filter(app): + return CapFilter(app, global_conf, method_to_call) + return cap_filter + +class CapFilter(object): + + def __init__(self, app, global_conf, method_to_call='upper'): + self.app = app + self.method_to_call = method_to_call + self.global_conf = global_conf + + def __call__(self, environ, start_response): + app_iter = self.app(environ, start_response) + for item in app_iter: + yield getattr(item, self.method_to_call)() + if hasattr(app_iter, 'close'): + app_iter.close() + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py new file mode 100644 index 0000000..ef13182 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py @@ -0,0 +1,14 @@ +class SimpleApp(object): + def __init__(self, global_conf, local_conf, name): + self.global_conf = global_conf + self.local_conf = local_conf + self.name = name + + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['I am: ', name] + + def make_app(cls, global_conf, **conf): + return cls(global_conf, conf, 'basic') + make_app = classmethod(make_app) + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py new file mode 100644 index 0000000..854483e --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + entry_points={ + 'paste.app_factory': """ + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + """, + 'paste.composit_factory': """ + remote_addr=fakeapp.apps:make_remote_addr + """, + 'paste.filter_factory': """ + caps=fakeapp.apps:make_cap_filter + """, + 'paste.filter_app_factory': """ + caps2=fakeapp.apps:CapFilter + """, + }, + ) diff --git a/tests/fixture.py b/tests/fixture.py new file mode 100644 index 0000000..751659d --- /dev/null +++ b/tests/fixture.py @@ -0,0 +1,20 @@ +import os +import sys +import shutil + +test_dir = os.path.dirname(__file__) +egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'EGG-INFO') +info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'FakeApp.egg-info') +if not os.path.exists(egg_info_dir): + try: + os.symlink(info_dir, egg_info_dir) + except: + shutil.copytree(info_dir, egg_info_dir) + +sys.path.append(os.path.dirname(egg_info_dir)) + +from pkg_resources import * +working_set.add_entry(os.path.dirname(egg_info_dir)) +require('FakeApp') diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini new file mode 100644 index 0000000..f1d931c --- /dev/null +++ b/tests/sample_configs/basic_app.ini @@ -0,0 +1,14 @@ +[application:main] +use = egg:FakeApp#basic_app + +[application:other] +use = egg:FakeApp#other + +[composit:remote_addr] +use = egg:FakeApp#remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 + diff --git a/tests/sample_configs/executable.ini b/tests/sample_configs/executable.ini new file mode 100755 index 0000000..3b75fe9 --- /dev/null +++ b/tests/sample_configs/executable.ini @@ -0,0 +1,10 @@ +#!/usr/bin/env paster +[exe] +sys.path = /fake/path/ + /another/fake/path ../fake_packages/ + +[server] +use = egg:PasteScript#cgi + +[app] +use = egg:FakeApp#basic_app diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini new file mode 100644 index 0000000..d614829 --- /dev/null +++ b/tests/sample_configs/test_config.ini @@ -0,0 +1,38 @@ +[DEFAULT] +def1 = a +def2 = b +basepath = %(here)s + +[app:test1] +use = egg:FakeApp#configed +setting1 = foo +setting2 = bar +apppath = %(basepath)s/app + +[app:test2] +use = egg:FakeApp#configed +set def1 = test2 +set another = TEST +local conf = something + +[app:test3] +use = test2 +set def1 = test3 +another = something more + across several + lines + +[app:test_foreign_config] +use = config:test_config_included.ini +set glob = override +another = FOO + +[app:test_get] +use = egg:FakeApp#configed +set def2 = TEST +get def1 = def1 +get foo = def2 + +[app:test_global_conf] +use = egg:FakeApp#configed +test_interp = this:%(inherit)s diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini new file mode 100644 index 0000000..cc0da7a --- /dev/null +++ b/tests/sample_configs/test_config_included.ini @@ -0,0 +1,10 @@ +[DEFAULT] +def2 = from include +def3 = c + +[app:main] +# Equivalent to the egg reference, but just for kicks... +paste.app_factory = fakeapp.configapps:SimpleApp.make_app +set glob = orig +bob = your uncle +another = BAR diff --git a/tests/sample_configs/test_error.ini b/tests/sample_configs/test_error.ini new file mode 100644 index 0000000..b6ad5b2 --- /dev/null +++ b/tests/sample_configs/test_error.ini @@ -0,0 +1,8 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:main] +use = egg:FakeApp#configed +setting1 = foo +setting2 = %(does_not_exist)s/bar diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini new file mode 100644 index 0000000..bfad8dc --- /dev/null +++ b/tests/sample_configs/test_filter.ini @@ -0,0 +1,22 @@ +[app:normal] +use = egg:FakeApp#basic_app + +[pipeline:piped] +pipeline = egg:FakeApp#caps normal + +[filter-app:filt] +use = egg:FakeApp#caps +method_to_call = lower +next = normal + +[pipeline:piped2] +pipeline = egg:FakeApp#caps2 normal + +[filter-app:filt2] +use = egg:FakeApp#caps2 +method_to_call = lower +next = normal + +[app:inv] +use = egg:FakeApp#basic_app +filter-with = egg:FakeApp#caps diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini new file mode 100644 index 0000000..118804f --- /dev/null +++ b/tests/sample_configs/test_filter_with.ini @@ -0,0 +1,12 @@ +[app:main] +use = egg:FakeApp#basic_app +example = test +filter-with = filter1 + +[filter:filter1] +use = egg:FakeApp#caps +filter-with = filter2 + +[filter:filter2] +use = egg:FakeApp#caps + diff --git a/tests/sample_configs/test_func.ini b/tests/sample_configs/test_func.ini new file mode 100644 index 0000000..a0d28c4 --- /dev/null +++ b/tests/sample_configs/test_func.ini @@ -0,0 +1,13 @@ +[application:main] +use = call:fakeapp.apps:make_basic_app + +[application:other] +use = call:fakeapp.apps:make_basic_app2 + +[composit:remote_addr] +use = call:fakeapp.apps:make_remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 \ No newline at end of file diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py new file mode 100644 index 0000000..1ddb52b --- /dev/null +++ b/tests/test_basic_app.py @@ -0,0 +1,36 @@ +from paste.deploy import loadapp + +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_main(): + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#ignored', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:sample_configs/basic_app.ini#other', + relative_to=here) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:sample_configs/basic_app.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..de40a2a --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,173 @@ +from nose.tools import eq_ + +from paste.deploy import loadapp, appconfig +from tests.fixture import * +import fakeapp.configapps as fc +import fakeapp.apps + + +ini_file = 'config:sample_configs/test_config.ini' +here = os.path.dirname(__file__) +config_path = os.path.join(here, 'sample_configs') +config_filename = os.path.join(config_path, 'test_config.ini') + + +def test_config_egg(): + app = loadapp('egg:FakeApp#configed') + assert isinstance(app, fc.SimpleApp) + + +def test_config1(): + app = loadapp(ini_file, relative_to=here, name='test1') + eq_(app.local_conf, { + 'setting1': 'foo', + 'setting2': 'bar', + 'apppath': os.path.join(config_path, 'app')}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'b', + 'basepath': config_path, + 'here': config_path, + '__file__': config_filename}) + + +def test_config2(): + app = loadapp(ini_file, relative_to=here, name='test2') + eq_(app.local_conf, { + 'local conf': 'something'}) + eq_(app.global_conf, { + 'def1': 'test2', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + # Run this to make sure the global-conf-modified test2 + # didn't mess up the general global conf + test_config1() + + +def test_config3(): + app = loadapp(ini_file, relative_to=here, name='test3') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'local conf': 'something', + 'another': 'something more\nacross several\nlines'}) + eq_(app.global_conf, { + 'def1': 'test3', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + test_config2() + + +def test_main(): + app = loadapp('config:test_func.ini', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#main', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#ignored', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:test_func.ini#other', relative_to=config_path) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:test_func.ini#remote_addr', relative_to=config_path) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 + + +def test_foreign_config(): + app = loadapp(ini_file, relative_to=here, name='test_foreign_config') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'another': 'FOO', + 'bob': 'your uncle'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'from include', + 'def3': 'c', + 'basepath': config_path, + 'glob': 'override', + 'here': config_path, + '__file__': os.path.join(config_path, 'test_config.ini')}) + + +def test_config_get(): + app = loadapp(ini_file, relative_to=here, name='test_get') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig(): + conf = appconfig(ini_file, relative_to=here, name='test_get') + eq_(conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename, + 'foo': 'TEST'}) + eq_(conf.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(conf.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig_filter_with(): + conf = appconfig('config:test_filter_with.ini', relative_to=config_path) + eq_(conf['example'], 'test') + + +def test_global_conf(): + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', + global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + eq_(conf, { + 'def1': 'a', + # Note that this gets overwritten: + 'def2': 'b', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + 'inherit': 'bazbar', + '__file__': config_filename, + 'test_interp': 'this:bazbar', + }) + eq_(conf.local_conf, { + 'test_interp': 'this:bazbar'}) + + +def test_interpolate_exception(): + try: + appconfig('config:test_error.ini', relative_to=config_path) + except Exception: + e = sys.exc_info()[1] + expected = "Error in file %s" % os.path.join(config_path, 'test_error.ini') + eq_(str(e).split(':')[0], expected) + else: + assert False, 'Should have raised an exception' diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py new file mode 100644 index 0000000..cc315e3 --- /dev/null +++ b/tests/test_config_middleware.py @@ -0,0 +1,28 @@ +from nose.tools import assert_raises +from nose.plugins.skip import SkipTest + +from paste.deploy.config import ConfigMiddleware + + +class Bug(Exception): + pass + + +def app_with_exception(environ, start_response): + def cont(): + yield "something" + raise Bug + start_response('200 OK', [('Content-type', 'text/html')]) + return cont() + + +def test_error(): + # This import is conditional due to Paste not yet working on py3k + try: + from paste.fixture import TestApp + except ImportError: + raise SkipTest + + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) + test_app = TestApp(wrapped) + assert_raises(Bug, test_app.get, '/') diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 0000000..5361310 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,17 @@ +def test_asbool_truthy(): + from paste.deploy.converters import asbool + assert asbool('true') + assert asbool('yes') + assert asbool('on') + assert asbool('y') + assert asbool('t') + assert asbool('1') + +def test_asbool_falsy(): + from paste.deploy.converters import asbool + assert not asbool('false') + assert not asbool('no') + assert not asbool('off') + assert not asbool('n') + assert not asbool('f') + assert not asbool('0') diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..a76af7c --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,53 @@ +from paste.deploy import loadapp +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_filter_app(): + app = loadapp('config:sample_configs/test_filter.ini#filt', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline(): + app = loadapp('config:sample_configs/test_filter.ini#piped', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app2(): + app = loadapp('config:sample_configs/test_filter.ini#filt2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline2(): + app = loadapp('config:sample_configs/test_filter.ini#piped2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app_inverted(): + app = loadapp('config:sample_configs/test_filter.ini#inv', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + + +def test_filter_with_filter_with(): + app = loadapp('config:sample_configs/test_filter_with.ini', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert isinstance(app.app, fakeapp.apps.CapFilter) + assert app.app.app is fakeapp.apps.basic_app diff --git a/tests/test_load_package.py b/tests/test_load_package.py new file mode 100644 index 0000000..b3fea55 --- /dev/null +++ b/tests/test_load_package.py @@ -0,0 +1,12 @@ +from pprint import pprint +import sys + +import pkg_resources + +from paste.deploy.compat import print_ + + +def test_load_package(): + print_('Path:') + pprint(sys.path) + print_(pkg_resources.require('FakeApp')) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4ac34fd --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py26,py27,py32,py33,pypy + +[testenv] +deps=nose + Paste +commands={envpython} setup.py test + +# Keep it this way until Paste has been ported to py3k +[testenv:py32] +deps=nose + +[testenv:py33] +deps=nose -- cgit v1.2.1