summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt10
-rw-r--r--CONTRIBUTORS.txt3
-rw-r--r--docs/tutorials/wiki/authorization.rst218
-rw-r--r--pyramid/authentication.py24
-rw-r--r--pyramid/paster.py31
-rw-r--r--pyramid/static.py61
-rw-r--r--pyramid/tests/test_integration.py59
-rw-r--r--pyramid/tests/test_paster.py75
-rw-r--r--pyramid/tests/test_static.py95
9 files changed, 413 insertions, 163 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 0bd19572a..3ae834d93 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -104,6 +104,16 @@ Bug Fixes
DummyRequest instead of eagerly assigning an attribute.
See also https://github.com/Pylons/pyramid/issues/165
+- When visiting a URL that represented a static view which resolved to a
+ subdirectory, the ``index.html`` of that subdirectory would not be served
+ properly. Instead, a redirect to ``/subdir`` would be issued. This has
+ been fixed, and now visiting a subdirectory that contains an ``index.html``
+ within a static view returns the index.html properly. See also
+ https://github.com/Pylons/pyramid/issues/67.
+
+- Redirects issued by a static view did not take into account any existing
+ ``SCRIPT_NAME`` (such as one set by a url mapping composite). Now they do.
+
1.0 (2011-01-30)
================
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 97cae7c3e..5a72f242e 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -132,3 +132,6 @@ Contributors
- Malthe Borch, 2011/02/28
- Joel Bohman, 2011/04/16
+
+- Juliusz Gonera, 2011/04/17
+
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index ee86eb543..e4480d6d9 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -7,21 +7,25 @@ edit, and add pages to our wiki. For purposes of demonstration we'll change
our application to allow people whom are members of a *group* named
``group:editors`` to add and edit wiki pages but we'll continue allowing
anyone with access to the server to view pages. :app:`Pyramid` provides
-facilities for *authorization* and *authentication*. We'll make use of both
-features to provide security to our application.
+facilities for :term:`authorization` and :term:`authentication`. We'll make
+use of both features to provide security to our application.
-The source code for this tutorial stage can be browsed via
-`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/
-<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_.
+We will add an :term:`authentication policy` and an
+:term:`authorization policy` to our :term:`application
+registry`, add a ``security.py`` module and give our :term:`root`
+resource an :term:`ACL`.
+Then we will add ``login`` and ``logout`` views, and modify the
+existing views to make them return a ``logged_in`` flag to the
+renderer and add :term:`permission` declarations to their ``view_config``
+decorators.
-Configuring a ``pyramid`` Authentication Policy
---------------------------------------------------
+Finally, we will add a ``login.pt`` template and change the existing
+``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in.
-For any :app:`Pyramid` application to perform authorization, we need to add a
-``security.py`` module and we'll need to change our :term:`application
-registry` to add an :term:`authentication policy` and a :term:`authorization
-policy`.
+The source code for this tutorial stage can be browsed via
+`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/
+<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_.
Adding Authentication and Authorization Policies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -64,6 +68,43 @@ user and groups sources. Note that the ``editor`` user is a member of the
``group:editors`` group in our dummy group data (the ``GROUPS`` data
structure).
+Giving Our Root Resource an ACL
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We need to give our root resource object an :term:`ACL`. This ACL will be
+sufficient to provide enough information to the :app:`Pyramid` security
+machinery to challenge a user who doesn't have appropriate credentials when
+he attempts to invoke the ``add_page`` or ``edit_page`` views.
+
+We need to perform some imports at module scope in our ``models.py`` file:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.security import Allow
+ from pyramid.security import Everyone
+
+Our root resource object is a ``Wiki`` instance. We'll add the following
+line at class scope to our ``Wiki`` class:
+
+.. code-block:: python
+ :linenos:
+
+ __acl__ = [ (Allow, Everyone, 'view'),
+ (Allow, 'group:editors', 'edit') ]
+
+It's only happenstance that we're assigning this ACL at class scope. An ACL
+can be attached to an object *instance* too; this is how "row level security"
+can be achieved in :app:`Pyramid` applications. We actually only need *one*
+ACL for the entire system, however, because our security requirements are
+simple, so this feature is not demonstrated.
+
+Our resulting ``models.py`` file will now look like so:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :linenos:
+ :language: python
+
Adding Login and Logout Views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -129,6 +170,38 @@ template. For example:
logged_in = logged_in,
edit_url = edit_url)
+Adding ``permission`` Declarations to our ``view_config`` Decorators
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To protect each of our views with a particular permission, we need to pass a
+``permission`` argument to each of our :class:`pyramid.view.view_config`
+decorators. To do so, within ``views.py``:
+
+- We add ``permission='view'`` to the decorator attached to the
+ ``view_wiki`` and ``view_page`` view functions. This makes the
+ assertion that only users who possess the ``view`` permission
+ against the context resource at the time of the request may
+ invoke these views. We've granted
+ :data:`pyramid.security.Everyone` the view permission at the
+ root model via its ACL, so everyone will be able to invoke the
+ ``view_wiki`` and ``view_page`` views.
+
+- We add ``permission='edit'`` to the decorator attached to the
+ ``add_page`` and ``edit_page`` view functions. This makes the
+ assertion that only users who possess the effective ``edit``
+ permission against the context resource at the time of the
+ request may invoke these views. We've granted the
+ ``group:editors`` principal the ``edit`` permission at the
+ root model via its ACL, so only a user whom is a member of
+ the group named ``group:editors`` will able to invoke the
+ ``add_page`` or ``edit_page`` views. We've likewise given
+ the ``editor`` user membership to this group via the
+ ``security.py`` file by mapping him to the ``group:editors``
+ group in the ``GROUPS`` data structure (``GROUPS
+ = {'editor':['group:editors']}``); the ``groupfinder``
+ function consults the ``GROUPS`` data structure. This means
+ that the ``editor`` user can add and edit pages.
+
Adding the ``login.pt`` Template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -154,92 +227,29 @@ class="app-welcome align-right">`` div:
<a href="${request.application_url}/logout">Logout</a>
</span>
-Giving Our Root Resource an ACL
--------------------------------
-
-We need to give our root resource object an :term:`ACL`. This ACL will be
-sufficient to provide enough information to the :app:`Pyramid` security
-machinery to challenge a user who doesn't have appropriate credentials when
-he attempts to invoke the ``add_page`` or ``edit_page`` views.
+Seeing Our Changes To ``views.py`` and our Templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We need to perform some imports at module scope in our ``models.py`` file:
+Our ``views.py`` module will look something like this when we're done:
-.. code-block:: python
+.. literalinclude:: src/authorization/tutorial/views.py
:linenos:
+ :language: python
- from pyramid.security import Allow
- from pyramid.security import Everyone
-
-Our root resource object is a ``Wiki`` instance. We'll add the following
-line at class scope to our ``Wiki`` class:
+Our ``edit.pt`` template will look something like this when we're done:
-.. code-block:: python
+.. literalinclude:: src/authorization/tutorial/templates/edit.pt
:linenos:
+ :language: xml
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
-
-It's only happenstance that we're assigning this ACL at class scope. An ACL
-can be attached to an object *instance* too; this is how "row level security"
-can be achieved in :app:`Pyramid` applications. We actually only need *one*
-ACL for the entire system, however, because our security requirements are
-simple, so this feature is not demonstrated.
-
-Our resulting ``models.py`` file will now look like so:
+Our ``view.pt`` template will look something like this when we're done:
-.. literalinclude:: src/authorization/tutorial/models.py
+.. literalinclude:: src/authorization/tutorial/templates/view.pt
:linenos:
- :language: python
-
-Adding ``permission`` Declarations to our ``view_config`` Decorators
---------------------------------------------------------------------
-
-To protect each of our views with a particular permission, we need to pass a
-``permission`` argument to each of our :class:`pyramid.view.view_config`
-decorators. To do so, within ``views.py``:
-
-- We add ``permission='view'`` to the decorator attached to the ``view_wiki``
- view function. This makes the assertion that only users who possess the
- ``view`` permission against the context resource at the time of the request
- may invoke this view. We've granted :data:`pyramid.security.Everyone` the
- view permission at the root model via its ACL, so everyone will be able to
- invoke the ``view_wiki`` view.
-
-- We add ``permission='view'`` to the decorator attached to the ``view_page``
- view function. This makes the assertion that only users who possess the
- effective ``view`` permission against the context resource at the time of
- the request may invoke this view. We've granted
- :data:`pyramid.security.Everyone` the view permission at the root model via
- its ACL, so everyone will be able to invoke the ``view_page`` view.
-
-- We add ``permission='edit'`` to the decorator attached to the ``add_page``
- view function. This makes the assertion that only users who possess the
- effective ``edit`` permission against the context resource at the time of
- the request may invoke this view. We've granted the ``group:editors``
- principal the ``edit`` permission at the root model via its ACL, so only
- the a user whom is a member of the group named ``group:editors`` will able
- to invoke the ``add_page`` view. We've likewise given the ``editor`` user
- membership to this group via thes ``security.py`` file by mapping him to
- the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS =
- {'editor':['group:editors']}``); the ``groupfinder`` function consults the
- ``GROUPS`` data structure. This means that the ``editor`` user can add
- pages.
-
-- We add ``permission='edit'`` to the decorator attached to the ``edit_page``
- view function. This makes the assertion that only users who possess the
- effective ``edit`` permission against the context resource at the time of
- the request may invoke this view. We've granted the ``group:editors``
- principal the ``edit`` permission at the root model via its ACL, so only
- the a user whom is a member of the group named ``group:editors`` will able
- to invoke the ``edit_page`` view. We've likewise given the ``editor`` user
- membership to this group via thes ``security.py`` file by mapping him to
- the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS =
- {'editor':['group:editors']}``); the ``groupfinder`` function consults the
- ``GROUPS`` data structure. This means that the ``editor`` user can edit
- pages.
+ :language: xml
Viewing the Application in a Browser
-------------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We can finally examine our application in a browser. The views we'll try are
as follows:
@@ -267,35 +277,7 @@ as follows:
credentials with the username ``editor``, password ``editor`` will
show the edit page form being displayed.
-Seeing Our Changes To ``views.py`` and our Templates
-----------------------------------------------------
-
-Our ``views.py`` module will look something like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/views.py
- :linenos:
- :language: python
-
-Our ``edit.pt`` template will look something like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/edit.pt
- :linenos:
- :language: xml
-
-Our ``view.pt`` template will look something like this when we're done:
-
-.. literalinclude:: src/authorization/tutorial/templates/view.pt
- :linenos:
- :language: xml
-
-Revisiting the Application
----------------------------
-
-When we revisit the application in a browser, and log in (as a result
-of hitting an edit or add page and submitting the login form with the
-``editor`` credentials), we'll see a Logout link in the upper right
-hand corner. When we click it, we're logged out, and redirected back
-to the front page.
-
-
-
+- After logging in (as a result of hitting an edit or add page and
+ submitting the login form with the ``editor`` credentials), we'll see
+ a Logout link in the upper right hand corner. When we click it,
+ we're logged out, and redirected back to the front page.
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 78349854b..a6c74e549 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -61,12 +61,12 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
``callback``
- Default: ``None``. A callback passed the :mod:`repoze.who`
- identity and the :term:`request`, expected to return ``None``
- if the user represented by the identity doesn't exist or a
- sequence of group identifiers (possibly empty) if the user
- does exist. If ``callback`` is None, the userid will be
- assumed to exist with no groups.
+ Default: ``None``. A callback passed the :mod:`repoze.who` identity
+ and the :term:`request`, expected to return ``None`` if the user
+ represented by the identity doesn't exist or a sequence of principal
+ identifiers (possibly empty) representing groups if the user does
+ exist. If ``callback`` is None, the userid will be assumed to exist
+ with no group principals.
Objects of this class implement the interface described by
:class:`pyramid.interfaces.IAuthenticationPolicy`.
@@ -149,10 +149,10 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
``callback``
Default: ``None``. A callback passed the userid and the request,
- expected to return None if the userid doesn't exist or a sequence
- of group identifiers (possibly empty) if the user does exist.
- If ``callback`` is None, the userid will be assumed to exist with no
- groups.
+ expected to return None if the userid doesn't exist or a sequence of
+ principal identifiers (possibly empty) representing groups if the
+ user does exist. If ``callback`` is None, the userid will be assumed
+ to exist with no group principals.
Objects of this class implement the interface described by
:class:`pyramid.interfaces.IAuthenticationPolicy`.
@@ -187,9 +187,9 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
Default: ``None``. A callback passed the userid and the
request, expected to return ``None`` if the userid doesn't
- exist or a sequence of group identifiers (possibly empty) if
+ exist or a sequence of principal identifiers (possibly empty) if
the user does exist. If ``callback`` is ``None``, the userid
- will be assumed to exist with no groups. Optional.
+ will be assumed to exist with no principals. Optional.
``cookie_name``
diff --git a/pyramid/paster.py b/pyramid/paster.py
index bc1573fb8..f9f8925d7 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -10,7 +10,7 @@ from paste.util.template import paste_script_template_renderer
from pyramid.scripting import get_root
class PyramidTemplate(Template):
- def pre(self, command, output_dir, vars): # pragma: no cover
+ def pre(self, command, output_dir, vars):
vars['random_string'] = os.urandom(20).encode('hex')
package_logger = vars['package']
if package_logger == 'root':
@@ -19,9 +19,12 @@ class PyramidTemplate(Template):
vars['package_logger'] = package_logger
return Template.pre(self, command, output_dir, vars)
- def post(self, *arg, **kw): # pragma: no cover
- print 'Welcome to Pyramid. Sorry for the convenience.'
- return Template.post(self, *arg, **kw)
+ def post(self, command, output_dir, vars):
+ self.out('Welcome to Pyramid. Sorry for the convenience.')
+ return Template.post(self, command, output_dir, vars)
+
+ def out(self, msg): # pragma: no cover (replaceable testing hook)
+ print msg
class StarterProjectTemplate(PyramidTemplate):
_template_dir = 'paster_templates/starter'
@@ -88,7 +91,7 @@ class PShellCommand(PCommand):
command will almost certainly fail.
"""
- summary = "Open an interactive shell with a pyramid app loaded"
+ summary = "Open an interactive shell with a Pyramid application loaded"
min_args = 2
max_args = 2
@@ -100,10 +103,11 @@ class PShellCommand(PCommand):
help="Don't use IPython even if it is available")
def command(self, IPShell=_marker):
- if IPShell is _marker:
- try: #pragma no cover
+ # IPShell passed to command method is for testing purposes
+ if IPShell is _marker: # pragma: no cover
+ try:
from IPython.Shell import IPShell
- except ImportError: #pragma no cover
+ except ImportError:
IPShell = None
cprt =('Type "help" for more information. "root" is the Pyramid app '
'root object, "registry" is the Pyramid registry object.')
@@ -113,16 +117,17 @@ class PShellCommand(PCommand):
app = self.get_app(config_file, section_name, loadapp=self.loadapp[0])
root, closer = self.get_root(app)
shell_globals = {'root':root, 'registry':app.registry}
- if IPShell is not None and not self.options.disable_ipython:
+
+ if (IPShell is None) or self.options.disable_ipython:
try:
- shell = IPShell(argv=[], user_ns=shell_globals)
- shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner
- shell.mainloop()
+ self.interact[0](banner, local=shell_globals)
finally:
closer()
else:
try:
- self.interact[0](banner, local=shell_globals)
+ shell = IPShell(argv=[], user_ns=shell_globals)
+ shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner
+ shell.mainloop()
finally:
closer()
diff --git a/pyramid/static.py b/pyramid/static.py
index 3866126ac..223652768 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -37,21 +37,18 @@ class PackageURLParser(StaticURLParser):
filename = request.path_info_pop(environ)
resource = os.path.normcase(os.path.normpath(
self.resource_name + '/' + filename))
- if ( (self.root_resource is not None) and
- (not resource.startswith(self.root_resource)) ):
+ if not resource.startswith(self.root_resource):
# Out of bounds
return self.not_found(environ, start_response)
if not pkg_resources.resource_exists(self.package_name, resource):
return self.not_found(environ, start_response)
if pkg_resources.resource_isdir(self.package_name, resource):
# @@: Cache?
- child_root = (self.root_resource is not None and
- self.root_resource or self.resource_name)
return self.__class__(
- self.package_name, resource, root_resource=child_root,
+ self.package_name, resource, root_resource=self.resource_name,
cache_max_age=self.cache_max_age)(environ, start_response)
- if (environ.get('PATH_INFO')
- and environ.get('PATH_INFO') != '/'): # pragma: no cover
+ pi = environ.get('PATH_INFO')
+ if pi and pi != '/':
return self.error_extra_path(environ, start_response)
full = pkg_resources.resource_filename(self.package_name, resource)
if_none_match = environ.get('HTTP_IF_NONE_MATCH')
@@ -154,8 +151,8 @@ class StaticURLInfo(object):
extra['view_permission'] = permission
extra['view'] = view
- # register a route using the computed view, permission, and pattern,
- # plus any extras passed to us via add_static_view
+ # register a route using the computed view, permission, and
+ # pattern, plus any extras passed to us via add_static_view
pattern = "%s*subpath" % name # name already ends with slash
self.config.add_route(name, pattern, **extra)
self.registrations.append((name, spec, False))
@@ -211,12 +208,44 @@ class static_view(object):
self.app = app
def __call__(self, context, request):
- subpath = '/'.join(request.subpath)
+ # Point PATH_INFO to the static file/dir path; point SCRIPT_NAME
+ # to the prefix before it.
+
+ # Postconditions:
+ # - SCRIPT_NAME and PATH_INFO are empty or start with /
+ # - At least one of SCRIPT_NAME or PATH_INFO are set.
+ # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
+ # be '/').
+
request_copy = request.copy()
- # Fix up PATH_INFO to get rid of everything but the "subpath"
- # (the actual path to the file relative to the root dir).
- request_copy.environ['PATH_INFO'] = '/' + subpath
- # Zero out SCRIPT_NAME for good measure.
- request_copy.environ['SCRIPT_NAME'] = ''
- return request_copy.get_response(self.app)
+ script_name = request_copy.environ.get('SCRIPT_NAME', '')
+ path_info = request_copy.environ.get('PATH_INFO', '/')
+
+ new_script_name = script_name
+ new_path_info = path_info
+
+ subpath = list(request.subpath)
+
+ if subpath:
+ # compute new_path_info
+ new_path_info = '/' + '/'.join(subpath)
+ if path_info.endswith('/'):
+ # readd trailing slash stripped by subpath (traversal)
+ # conversion
+ new_path_info += '/'
+
+ # compute new_script_name
+ tmp = []
+ workback = (script_name + path_info).split('/')
+ while workback:
+ el = workback.pop()
+ if el:
+ tmp.insert(0, el)
+ if tmp == subpath:
+ new_script_name = '/'.join(workback)
+ break
+
+ request_copy.environ['SCRIPT_NAME'] = new_script_name
+ request_copy.environ['PATH_INFO'] = new_path_info
+ return request_copy.get_response(self.app)
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index dc7525080..095f22f41 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -45,7 +45,7 @@ here = os.path.dirname(__file__)
staticapp = static(os.path.join(here, 'fixtures'))
class TestStaticApp(unittest.TestCase):
- def test_it(self):
+ def test_basic(self):
from webob import Request
context = DummyContext()
from StringIO import StringIO
@@ -57,13 +57,68 @@ class TestStaticApp(unittest.TestCase):
'wsgi.version':(1,0),
'wsgi.url_scheme':'http',
'wsgi.input':StringIO()})
- request.subpath = ['minimal.pt']
+ request.subpath = ('minimal.pt',)
result = staticapp(context, request)
self.assertEqual(result.status, '200 OK')
self.assertEqual(
result.body.replace('\r', ''),
open(os.path.join(here, 'fixtures/minimal.pt'), 'r').read())
+ def test_file_in_subdir(self):
+ from webob import Request
+ context = DummyContext()
+ from StringIO import StringIO
+ request = Request({'PATH_INFO':'',
+ 'SCRIPT_NAME':'',
+ 'SERVER_NAME':'localhost',
+ 'SERVER_PORT':'80',
+ 'REQUEST_METHOD':'GET',
+ 'wsgi.version':(1,0),
+ 'wsgi.url_scheme':'http',
+ 'wsgi.input':StringIO()})
+ request.subpath = ('static', 'index.html',)
+ result = staticapp(context, request)
+ self.assertEqual(result.status, '200 OK')
+ self.assertEqual(
+ result.body.replace('\r', ''),
+ open(os.path.join(here, 'fixtures/static/index.html'), 'r').read())
+
+ def test_redirect_to_subdir(self):
+ from webob import Request
+ context = DummyContext()
+ from StringIO import StringIO
+ request = Request({'PATH_INFO':'',
+ 'SCRIPT_NAME':'',
+ 'SERVER_NAME':'localhost',
+ 'SERVER_PORT':'80',
+ 'REQUEST_METHOD':'GET',
+ 'wsgi.version':(1,0),
+ 'wsgi.url_scheme':'http',
+ 'wsgi.input':StringIO()})
+ request.subpath = ('static',)
+ result = staticapp(context, request)
+ self.assertEqual(result.status, '301 Moved Permanently')
+ self.assertEqual(result.location, 'http://localhost/static/')
+
+ def test_redirect_to_subdir_with_existing_script_name(self):
+ from webob import Request
+ context = DummyContext()
+ from StringIO import StringIO
+ request = Request({'PATH_INFO':'',
+ 'SCRIPT_NAME':'/script_name',
+ 'SERVER_NAME':'localhost',
+ 'SERVER_PORT':'80',
+ 'REQUEST_METHOD':'GET',
+ 'wsgi.version':(1,0),
+ 'wsgi.url_scheme':'http',
+ 'wsgi.input':StringIO()})
+ request.subpath = ['static']
+ result = staticapp(context, request)
+ self.assertEqual(result.status, '301 Moved Permanently')
+ self.assertEqual(result.location,
+ 'http://localhost/script_name/static/')
+
+
class IntegrationBase(unittest.TestCase):
root_factory = None
package = None
diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py
index 35349b7c7..07ec4f7b7 100644
--- a/pyramid/tests/test_paster.py
+++ b/pyramid/tests/test_paster.py
@@ -1,5 +1,39 @@
import unittest
+class TestPyramidTemplate(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.paster import PyramidTemplate
+ return PyramidTemplate
+
+ def _makeOne(self, name):
+ cls = self._getTargetClass()
+ return cls(name)
+
+ def test_pre_logger_eq_root(self):
+ tmpl = self._makeOne('name')
+ vars = {'package':'root'}
+ result = tmpl.pre(None, None, vars)
+ self.assertEqual(result, None)
+ self.assertEqual(vars['package_logger'], 'app')
+ self.failUnless(len(vars['random_string']) == 40)
+
+ def test_pre_logger_noteq_root(self):
+ tmpl = self._makeOne('name')
+ vars = {'package':'notroot'}
+ result = tmpl.pre(None, None, vars)
+ self.assertEqual(result, None)
+ self.assertEqual(vars['package_logger'], 'notroot')
+ self.failUnless(len(vars['random_string']) == 40)
+
+ def test_post(self):
+ tmpl = self._makeOne('name')
+ vars = {'package':'root'}
+ L = []
+ tmpl.out = lambda msg: L.append(msg)
+ result = tmpl.post(None, None, vars)
+ self.assertEqual(result, None)
+ self.assertEqual(L, ['Welcome to Pyramid. Sorry for the convenience.'])
+
class TestPShellCommand(unittest.TestCase):
def _getTargetClass(self):
from pyramid.paster import PShellCommand
@@ -8,7 +42,7 @@ class TestPShellCommand(unittest.TestCase):
def _makeOne(self):
return self._getTargetClass()('pshell')
- def test_command_ipython_disabled(self):
+ def test_command_ipshell_is_None_ipython_enabled(self):
command = self._makeOne()
interact = DummyInteractor()
app = DummyApp()
@@ -18,7 +52,7 @@ class TestPShellCommand(unittest.TestCase):
command.args = ('/foo/bar/myapp.ini', 'myapp')
class Options(object): pass
command.options = Options()
- command.options.disable_ipython =True
+ command.options.disable_ipython = False
command.command(IPShell=None)
self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini')
self.assertEqual(loadapp.section_name, 'myapp')
@@ -32,6 +66,30 @@ class TestPShellCommand(unittest.TestCase):
self.failUnless(interact.banner)
self.assertEqual(len(app.threadlocal_manager.popped), 1)
+ def test_command_ipshell_is_not_None_ipython_disabled(self):
+ command = self._makeOne()
+ interact = DummyInteractor()
+ app = DummyApp()
+ loadapp = DummyLoadApp(app)
+ command.interact = (interact,)
+ command.loadapp = (loadapp,)
+ command.args = ('/foo/bar/myapp.ini', 'myapp')
+ class Options(object): pass
+ command.options = Options()
+ command.options.disable_ipython = True
+ command.command(IPShell='notnone')
+ self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini')
+ self.assertEqual(loadapp.section_name, 'myapp')
+ self.failUnless(loadapp.relative_to)
+ self.assertEqual(len(app.threadlocal_manager.pushed), 1)
+ pushed = app.threadlocal_manager.pushed[0]
+ self.assertEqual(pushed['registry'], dummy_registry)
+ self.assertEqual(pushed['request'].registry, dummy_registry)
+ self.assertEqual(interact.local, {'root':dummy_root,
+ 'registry':dummy_registry})
+ self.failUnless(interact.banner)
+ self.assertEqual(len(app.threadlocal_manager.popped), 1)
+
def test_command_ipython_enabled(self):
command = self._makeOne()
app = DummyApp()
@@ -133,6 +191,19 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(result, None)
self.assertEqual(L, [])
+ def test_no_mapper(self):
+ command = self._makeOne()
+ command._get_mapper = lambda *arg:None
+ L = []
+ command.out = L.append
+ app = DummyApp()
+ loadapp = DummyLoadApp(app)
+ command.loadapp = (loadapp,)
+ command.args = ('/foo/bar/myapp.ini', 'myapp')
+ result = command.command()
+ self.assertEqual(result, None)
+ self.assertEqual(L, [])
+
def test_single_route_no_route_registered(self):
command = self._makeOne()
route = DummyRoute('a', '/a')
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index acf5a754b..9c4c4a1c8 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -92,6 +92,14 @@ class TestPackageURLParser(unittest.TestCase):
body = response[0]
self.failUnless('<html>static</html>' in body)
+ def test_resource_has_extra_path_info(self):
+ environ = self._makeEnviron(PATH_INFO='/static/index.html/more')
+ inst = self._makeOne('pyramid.tests', 'fixtures')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless("The trailing path '/more' is not allowed" in body)
+
def test_resource_is_file_with_cache_max_age(self):
environ = self._makeEnviron(PATH_INFO='/index.html')
inst = self._makeOne('pyramid.tests', 'fixtures/static',
@@ -122,6 +130,15 @@ class TestPackageURLParser(unittest.TestCase):
['Accept-Ranges', 'Content-Length', 'Content-Range',
'Content-Type', 'ETag', 'Last-Modified'])
+ def test_with_root_resource(self):
+ environ = self._makeEnviron(PATH_INFO='/static/index.html')
+ inst = self._makeOne('pyramid.tests', 'fixtures',
+ root_resource='fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>static</html>' in body)
+
def test_if_none_match(self):
class DummyEq(object):
def __eq__(self, other):
@@ -136,6 +153,18 @@ class TestPackageURLParser(unittest.TestCase):
self.assertEqual(sr.headerlist[0][0], 'ETag')
self.assertEqual(response[0], '')
+ def test_if_none_match_miss(self):
+ class DummyEq(object):
+ def __eq__(self, other):
+ return False
+ dummy_eq = DummyEq()
+ environ = self._makeEnviron(HTTP_IF_NONE_MATCH=dummy_eq)
+ inst = self._makeOne('pyramid.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ inst(environ, sr)
+ self.assertEqual(len(sr.headerlist), 6)
+ self.assertEqual(sr.status, '200 OK')
+
def test_repr(self):
import os.path
inst = self._makeOne('pyramid.tests', 'fixtures/static')
@@ -230,6 +259,53 @@ class Test_static_view(unittest.TestCase):
self.assertEqual(response.package_name, 'another')
self.assertEqual(response.cache_max_age, 3600)
+ def test_no_subpath_preserves_path_info_and_script_name(self):
+ view = self._makeOne('fixtures', package_name='another')
+ context = DummyContext()
+ request = DummyRequest()
+ request.subpath = ()
+ request.environ = self._makeEnviron(PATH_INFO='/path_info',
+ SCRIPT_NAME='/script_name')
+ view(context, request)
+ self.assertEqual(request.copied, True)
+ self.assertEqual(request.environ['PATH_INFO'], '/path_info')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/script_name')
+
+ def test_with_subpath_path_info_ends_with_slash(self):
+ view = self._makeOne('fixtures', package_name='another')
+ context = DummyContext()
+ request = DummyRequest()
+ request.subpath = ('subpath',)
+ request.environ = self._makeEnviron(PATH_INFO='/path_info/subpath/')
+ view(context, request)
+ self.assertEqual(request.copied, True)
+ self.assertEqual(request.environ['PATH_INFO'], '/subpath/')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info')
+
+ def test_with_subpath_original_script_name_preserved(self):
+ view = self._makeOne('fixtures', package_name='another')
+ context = DummyContext()
+ request = DummyRequest()
+ request.subpath = ('subpath',)
+ request.environ = self._makeEnviron(PATH_INFO='/path_info/subpath/',
+ SCRIPT_NAME='/scriptname')
+ view(context, request)
+ self.assertEqual(request.copied, True)
+ self.assertEqual(request.environ['PATH_INFO'], '/subpath/')
+ self.assertEqual(request.environ['SCRIPT_NAME'],
+ '/scriptname/path_info')
+
+ def test_with_subpath_new_script_name_fixes_trailing_double_slashes(self):
+ view = self._makeOne('fixtures', package_name='another')
+ context = DummyContext()
+ request = DummyRequest()
+ request.subpath = ('sub', 'path')
+ request.environ = self._makeEnviron(PATH_INFO='/path_info//sub//path//')
+ view(context, request)
+ self.assertEqual(request.copied, True)
+ self.assertEqual(request.environ['PATH_INFO'], '/sub/path/')
+ self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info/')
+
class TestStaticURLInfo(unittest.TestCase):
def _getTargetClass(self):
from pyramid.static import StaticURLInfo
@@ -258,6 +334,14 @@ class TestStaticURLInfo(unittest.TestCase):
request = DummyRequest()
self.assertRaises(ValueError, inst.generate, 'path', request)
+ def test_generate_registration_miss(self):
+ inst = self._makeOne(None)
+ inst.registrations = [('name', 'spec', False),
+ ('http://example.com/foo/', 'package:path/',True)]
+ request = DummyRequest()
+ result = inst.generate('package:path/abc', request)
+ self.assertEqual(result, 'http://example.com/foo/abc')
+
def test_generate_slash_in_name1(self):
inst = self._makeOne(None)
inst.registrations = [('http://example.com/foo/', 'package:path/',True)]
@@ -333,6 +417,17 @@ class TestStaticURLInfo(unittest.TestCase):
permission='abc')
self.assertEqual(config.kw['view_permission'], 'abc')
+ def test_add_viewname_with_view_permission(self):
+ class Config:
+ def add_route(self, *arg, **kw):
+ self.arg = arg
+ self.kw = kw
+ config = Config()
+ inst = self._makeOne(config)
+ inst.add('view', 'anotherpackage:path', cache_max_age=1,
+ view_permission='abc')
+ self.assertEqual(config.kw['view_permission'], 'abc')
+
class DummyStartResponse:
def __call__(self, status, headerlist, exc_info=None):
self.status = status