diff options
26 files changed, 585 insertions, 105 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 85dd3be2a..2366522df 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,83 @@ +next release +============ + +Features +-------- + +- ``ACLAuthorizationPolicy`` supports ``__acl__`` as a callable. This + removes the ambiguity between the potential ``AttributeError`` that would + be raised on the ``context`` when the property was not defined and the + ``AttributeError`` that could be raised from any user-defined code within + a dynamic property. It is recommended to define a dynamic ACL as a callable + to avoid this ambiguity. See https://github.com/Pylons/pyramid/issues/735. + +- Allow a protocol-relative URL (e.g. ``//example.com/images``) to be passed to + ``pyramid.config.Configurator.add_static_view``. This allows + externally-hosted static URLs to be generated based on the current protocol. + +- The ``AuthTktAuthenticationPolicy`` now supports IPv6 addresses when using + the ``include_ip=True`` option. This is possibly incompatible with + alternative ``auth_tkt`` implementations, as the specification does not + define how to properly handle IPv6. See + https://github.com/Pylons/pyramid/issues/831. + +- Make it possible to use variable arguments via + ``pyramid.paster.get_appsettings``. This also allowed the generated + ``initialize_db`` script from the ``alchemy`` scaffold to grow support + for options in the form ``a=1 b=2`` so you can fill in + values in a parameterized ``.ini`` file, e.g. + ``initialize_myapp_db etc/development.ini a=1 b=2``. + See https://github.com/Pylons/pyramid/pull/911 + +Bug Fixes +--------- + +- View lookup will now search for valid views based on the inheritance + hierarchy of the context. It tries to find views based on the most + specific context first, and upon predicate failure, will move up the + inheritance chain to test views found by the super-type of the context. + In the past, only the most specific type containing views would be checked + and if no matching view could be found then a PredicateMismatch would be + raised. Now predicate mismatches don't hide valid views registered on + super-types. Here's an example that now works:: + + .. code-block:: python + + class IResource(Interface): + + ... + + @view_config(context=IResource) + def get(context, request): + + ... + + @view_config(context=IResource, request_method='POST') + def post(context, request): + + ... + + @view_config(context=IResource, request_method='DELETE') + def delete(context, request): + + ... + + @implementor(IResource) + class MyResource: + + ... + + @view_config(context=MyResource, request_method='POST') + def override_post(context, request): + + ... + + Previously the override_post view registration would hide the get + and delete views in the context of MyResource -- leading to a + predicate mismatch error when trying to use GET or DELETE + methods. Now the views are found and no predicate mismatch is + raised. + 1.4 (2012-12-18) ================ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 971c172f8..94eee9443 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -192,3 +192,7 @@ Contributors - Robert Jackiewicz, 2012/11/12 - John Anderson, 2012/11/14 + +- Bert JW Regeer, 2013/02/01 + +- Georges Dubus, 2013/03/21 diff --git a/docs/api/paster.rst b/docs/api/paster.rst index bde128e05..edc3738fc 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -9,6 +9,6 @@ .. autofunction:: get_app(config_uri, name=None, options=None) - .. autofunction:: get_appsettings(config_uri, name=None) + .. autofunction:: get_appsettings(config_uri, name=None, options=None) .. autofunction:: setup_logging(config_uri) diff --git a/docs/conf.py b/docs/conf.py index 8d22d4d42..eff6db488 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,13 +55,14 @@ extensions = [ # Looks for objects in external projects intersphinx_mapping = { - 'who': ('http://docs.repoze.org/who/2.0', None), - 'python': ('http://docs.python.org', None), - 'python3': ('http://docs.python.org/3', None), - 'tstring': + 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), + 'who': ('http://docs.repoze.org/who/latest', None), + 'python': ('http://docs.python.org', None), + 'python3': ('http://docs.python.org/3', None), + 'tstring': ('http://docs.pylonsproject.org/projects/translationstring/en/latest', None), - 'venusian': + 'venusian': ('http://docs.pylonsproject.org/projects/venusian/en/latest', None), } diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 04a060ac3..0a03d9170 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -19,13 +19,32 @@ run :app:`Pyramid`. run under any version of Python before 2.6. :app:`Pyramid` is known to run on all popular UNIX-like systems such as -Linux, MacOS X, and FreeBSD as well as on Windows platforms. It is also -known to run on :term:`PyPy` (1.9+). +Linux, Mac OS X, and FreeBSD as well as on Windows platforms. It is +also known to run on :term:`PyPy` (1.9+). :app:`Pyramid` installation does not require the compilation of any C code, so you need only a Python interpreter that meets the requirements mentioned. +For Mac OS X Users +~~~~~~~~~~~~~~~~~~ + +From `Python.org <http://python.org/download/mac/>`_: + + Python comes pre-installed on Mac OS X, but due to Apple's release + cycle, it's often one or even two years old. The overwhelming + recommendation of the "MacPython" community is to upgrade your + Python by downloading and installing a newer version from + `the Python standard release page <http://python.org/download/releases/>`_. + +It is recommended to download one of the *installer* versions, unless you prefer to install your Python through a packgage manager (e.g., macports or homebrew) or to build your Python from source. + +Unless you have a need for a specific earlier version, it is recommended +to install the latest 2.x or 3.x version of Python. + +If you use an installer for your Python, then you can skip to the +section :ref:`installing_unix`. + If You Don't Yet Have A Python Interpreter (UNIX) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -269,7 +288,7 @@ you can then create a virtual environment. To do so, invoke the following: .. code-block:: text - $ export $VENV=~/env + $ export VENV=~/env $ virtualenv --no-site-packages $VENV New python executable in /home/foo/env/bin/python Installing setuptools.............done. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 5b79edd19..203aa2404 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -234,8 +234,8 @@ class: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): __acl__ = [ @@ -250,8 +250,8 @@ Or, if your resources are persistent, an ACL might be specified via the .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): pass @@ -270,6 +270,27 @@ resource instances with an ACL (as opposed to just decorating their class) in applications such as "CMS" systems where fine-grained access is required on an object-by-object basis. +Dynamic ACLs are also possible by turning the ACL into a callable on the +resource. This may allow the ACL to dynamically generate rules based on +properties of the instance. + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + + class Blog(object): + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, self.owner, 'edit'), + (Allow, 'group:editors', 'edit'), + ] + + def __init__(self, owner): + self.owner = owner + .. index:: single: ACE single: access control entry @@ -282,8 +303,8 @@ Here's an example ACL: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -321,9 +342,9 @@ order dictated by the ACL*. So if you have an ACL like this: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow from pyramid.security import Deny + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -359,8 +380,8 @@ ACE, as below. .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 68be4ee7c..eb2445864 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -43,9 +43,9 @@ above is executed. It accepts some settings and returns a :term:`WSGI` application. (See :ref:`startup_chapter` for more about ``pserve``.) The main function first creates a :term:`SQLAlchemy` database engine using -``engine_from_config`` from the ``sqlalchemy.`` prefixed settings in the -``development.ini`` file's ``[app:main]`` section. This will be a URI -(something like ``sqlite://``): +:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed +settings in the ``development.ini`` file's ``[app:main]`` section. +This will be a URI (something like ``sqlite://``): .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 13 @@ -226,7 +226,7 @@ To give a simple example of a model class, we define one named ``MyModel``: :linenos: :language: py -Our example model has an ``__init__`` method that takes a two arguments +Our example model has an ``__init__`` method that takes two arguments (``name``, and ``value``). It stores these values as ``self.name`` and ``self.value`` within the ``__init__`` function itself. The ``MyModel`` class also has a diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 99f7969bc..60427a911 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -34,7 +34,7 @@ sample and we're not going to use it. Then, we added a ``Page`` class. Because this is a SQLAlchemy application, this class inherits from an instance of -:class:`sqlalchemy.ext.declarative.declarative_base`. +:func:`sqlalchemy.ext.declarative.declarative_base`. .. literalinclude:: src/models/tutorial/models.py :pyobject: Page @@ -45,9 +45,10 @@ As you can see, our ``Page`` class has a class level attribute ``__tablename__`` which equals the string ``'pages'``. This means that SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our ``Page`` class will also have class-level attributes named ``id``, ``name`` and -``data`` (all instances of :class:`sqlalchemy.Column`). These will map to -columns in the ``pages`` table. The ``id`` attribute will be the primary key -in the table. The ``name`` attribute will be a text attribute, each value of +``data`` (all instances of :class:`sqlalchemy.schema.Column`). +These will map to columns in the ``pages`` table. +The ``id`` attribute will be the primary key in the table. +The ``name`` attribute will be a text attribute, each value of which needs to be unique within the column. The ``data`` attribute is a text attribute that will hold the body of each page. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 5727816c8..f2ac2f85f 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -126,7 +126,7 @@ The ``view_page`` view function ------------------------------- ``view_page()`` is used to display a single page of our -wiki. It renders the :term:`ReStructuredText` body of a page (stored as +wiki. It renders the :term:`reStructuredText` body of a page (stored as the ``data`` attribute of a ``Page`` model object) as HTML. Then it substitutes an HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 255a60ec2..e646f63d2 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -2,34 +2,93 @@ Installation ============ -Preparation -=========== +Before You Begin +================ + +This tutorial assumes that you have already followed the steps in +:ref:`installing_chapter`, thereby satisfying the following +requirements. + +* Python interpreter is installed on your operating system +* :term:`setuptools` or :term:`distribute` is installed +* :term:`virtualenv` is installed + +Create and Use a Virtual Python Environment +------------------------------------------- + +Next let's create a `virtualenv` workspace for our project. We will +use the `VENV` environment variable instead of absolute path of the +virtual environment. -Follow the steps in :ref:`installing_chapter`, but name the virtualenv -directory ``pyramidtut``. +**On UNIX:** -Preparation, UNIX ------------------ +.. code-block:: text + + $ export VENV=~/pyramidtut + $ virtualenv --no-site-packages $VENV + New python executable in /home/foo/env/bin/python + Installing setuptools.............done. + +**On Windows:** -#. Install SQLite3 and its development packages if you don't already - have them installed. Usually this is via your system's package - manager. On a Debian system, this would be: +Set the `VENV` environment variable. + +.. code-block:: text + + c:\> set VENV=c:\pyramidtut + +Versions of Python use different paths, so you will need to adjust the +path to the command for your Python version. + +Python 2.7: + +.. code-block:: text + + c:\> c:\Python27\Scripts\virtualenv --no-site-packages %VENV% + +Python 3.2: + +.. code-block:: text + + c:\> c:\Python32\Scripts\virtualenv --no-site-packages %VENV% + +Install Pyramid Into the Virtual Python Environment +--------------------------------------------------- + +**On UNIX:** + +.. code-block:: text + + $ $VENV/bin/easy_install pyramid + +**On Windows** + +.. code-block:: text + + c:\env> %VENV%\Scripts\easy_install pyramid + +SQLite3 +------- + +Install SQLite3 and its development packages if you don't already +have them installed. Usually this is via your system's package +manager. On a Debian system, this would be: .. code-block:: text $ sudo apt-get install libsqlite3-dev -#. Switch to the ``pyramidtut`` directory: +Entering the virtualenv +----------------------- + +Do not forget to switch to the ``pyramidtut`` directory. +In order to do so, run this command if you are on Unix: .. code-block:: text $ cd pyramidtut - -Preparation, Windows --------------------- - -#. Switch to the ``pyramidtut`` directory: +And run this if you are on Windows: .. code-block:: text @@ -40,10 +99,20 @@ Preparation, Windows Making a Project ================ -Your next step is to create a project. For this tutorial, we will use the -:term:`scaffold` named ``alchemy``, which generates an application -that uses :term:`SQLAlchemy` and :term:`URL dispatch`. :app:`Pyramid` -supplies a variety of scaffolds to generate sample projects. +Your next step is to create a project. For this tutorial we will use +the :term:`scaffold` named ``alchemy`` which generates an application +that uses :term:`SQLAlchemy` and :term:`URL dispatch`. + +:app:`Pyramid` supplies a variety of scaffolds to generate sample +projects. We will use `pcreate`—a script that comes with Pyramid to +quickly and easily generate scaffolds usually with a single command—to +create the scaffold for our project. + +By passing in `alchemy` into the `pcreate` command, the script creates +the files needed to use SQLAlchemy. By passing in our application name +`tutorial`, the script inserts that application name into all the +required files. For example, `pcreate` creates the +``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory. The below instructions assume your current working directory is the "virtualenv" named "pyramidtut". @@ -66,11 +135,10 @@ On Windows: startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. - .. _installing_project_in_dev_mode: -Installing the Project in "Development Mode" -============================================ +Installing the Project in Development Mode +========================================== In order to do development on the project easily, you must "register" the project as a development egg in your workspace using the @@ -92,8 +160,9 @@ On Windows: c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -Success executing this command will end with a line to the console something -like:: +The console will show `setup.py` checking for packages and installing +missing packages. Success executing this command will show a line like +the following:: Finished processing dependencies for tutorial==0.0 diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 4f6ed2c1d..bc0286ed3 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -450,6 +450,12 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Default: ``False``. Make the requesting IP address part of the authentication data in the cookie. Optional. + For IPv6 this option is not recommended. The ``mod_auth_tkt`` + specification does not specify how to handle IPv6 addresses, so using + this option in combination with IPv6 addresses may cause an + incompatible cookie. It ties the authentication ticket to that + individual's IPv6 address. + ``timeout`` Default: ``None``. Maximum number of seconds which a newly @@ -736,9 +742,17 @@ def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, tokens = bytes_(tokens, 'utf-8') user_data = bytes_(user_data, 'utf-8') hash_obj = hashlib.new(hashalg) - hash_obj.update( - encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0' - + tokens + b'\0' + user_data) + + # Check to see if this is an IPv6 address + if ':' in ip: + ip_timestamp = ip + str(int(timestamp)) + ip_timestamp = bytes_(ip_timestamp) + else: + # encode_ip_timestamp not required, left in for backwards compatibility + ip_timestamp = encode_ip_timestamp(ip, timestamp) + + hash_obj.update(ip_timestamp + secret + userid + b'\0' + + tokens + b'\0' + user_data) digest = hash_obj.hexdigest() hash_obj2 = hashlib.new(hashalg) hash_obj2.update(bytes_(digest) + secret) diff --git a/pyramid/authorization.py b/pyramid/authorization.py index 943f8bd00..1fd05e244 100644 --- a/pyramid/authorization.py +++ b/pyramid/authorization.py @@ -80,6 +80,9 @@ class ACLAuthorizationPolicy(object): except AttributeError: continue + if acl and callable(acl): + acl = acl() + for ace in acl: ace_action, ace_principal, ace_permissions = ace if ace_principal in principals: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c53b2b091..1c7620e67 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1793,6 +1793,10 @@ class ViewsConfiguratorMixin(object): qualified URL (e.g. starts with ``http://`` or similar). In this mode, the ``name`` is used as the prefix of the full URL when generating a URL using :meth:`pyramid.request.Request.static_url`. + Furthermore, if a protocol-relative URL (e.g. ``//example.com/images``) + is used as the ``name`` argument, the generated URL will use the + protocol of the request (http or https, respectively). + For example, if ``add_static_view`` is called like so: .. code-block:: python @@ -1801,20 +1805,14 @@ class ViewsConfiguratorMixin(object): Subsequently, the URLs generated by :meth:`pyramid.request.Request.static_url` for that static view will - be prefixed with ``http://example.com/images``: + be prefixed with ``http://example.com/images`` (the external webserver + listening on ``example.com`` must be itself configured to respond + properly to such a request.): .. code-block:: python static_url('mypackage:images/logo.png', request) - When ``add_static_view`` is called with a ``name`` argument that is - the URL ``http://example.com/images``, subsequent calls to - :meth:`pyramid.request.Request.static_url` with paths that start with - the ``path`` argument passed to ``add_static_view`` will generate a - URL something like ``http://example.com/logo.png``. The external - webserver listening on ``example.com`` must be itself configured to - respond properly to such a request. - See :ref:`static_assets_section` for more information. """ spec = self._make_spec(path) @@ -1858,6 +1856,12 @@ class StaticURLInfo(object): kw['subpath'] = subpath return request.route_url(route_name, **kw) else: + parsed = url_parse(url) + if not parsed.scheme: + # parsed.scheme is readonly, so we have to parse again + # to change the scheme, sigh. + url = urlparse.urlunparse(url_parse( + url, scheme=request.environ['wsgi.url_scheme'])) subpath = url_quote(subpath) return urljoin(url, subpath) @@ -1886,7 +1890,7 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - if url_parse(name)[0]: + if url_parse(name).netloc: # it's a URL # url, spec, route_name url = name diff --git a/pyramid/paster.py b/pyramid/paster.py index ce07d1fe0..967543849 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -23,26 +23,34 @@ def get_app(config_uri, name=None, options=None, loadapp=loadapp): path, section = _getpathsec(config_uri, name) config_name = 'config:%s' % path here_dir = os.getcwd() - if options: - kw = {'global_conf': options} - else: - kw = {} - app = loadapp(config_name, name=section, relative_to=here_dir, **kw) + app = loadapp( + config_name, + name=section, + relative_to=here_dir, + global_conf=options) return app -def get_appsettings(config_uri, name=None, appconfig=appconfig): +def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): """ Return a dictionary representing the key/value pairs in an ``app`` section within the file represented by ``config_uri``. + ``options``, if passed, should be a dictionary used as variable assignments + like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is + used in the config file. + If the ``name`` is None, this will attempt to parse the name from the ``config_uri`` string expecting the format ``inifile#name``. If no name is found, the name will default to "main".""" path, section = _getpathsec(config_uri, name) config_name = 'config:%s' % path here_dir = os.getcwd() - return appconfig(config_name, name=section, relative_to=here_dir) + return appconfig( + config_name, + name=section, + relative_to=here_dir, + global_conf=options) def setup_logging(config_uri, fileConfig=fileConfig, configparser=configparser): diff --git a/pyramid/router.py b/pyramid/router.py index 9b6138ea9..63c12a1af 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,4 +1,5 @@ from zope.interface import ( + Interface, implementer, providedBy, ) @@ -24,6 +25,7 @@ from pyramid.events import ( NewResponse, ) +from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager @@ -158,8 +160,23 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: - response = view_callable(context, request) - + try: + response = view_callable(context, request) + except PredicateMismatch: + # look for other views that meet the predicate + # criteria + for iface in context_iface.flattened(): + view_callable = adapters.lookup( + (IViewClassifier, request.request_iface, iface), + IView, name=view_name, default=None) + if view_callable is not None: + try: + response = view_callable(context, request) + break + except PredicateMismatch: + pass + else: + raise return response def invoke_subrequest(self, request, use_tweens=False): diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 66feb3008..7dfdece15 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -9,6 +9,8 @@ from pyramid.paster import ( setup_logging, ) +from pyramid.scripts.common import parse_vars + from ..models import ( DBSession, MyModel, @@ -18,17 +20,18 @@ from ..models import ( def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s <config_uri>\n' + print('usage: %s <config_uri> [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.create_all(engine) diff --git a/pyramid/tests/fixtures/dummy.ini b/pyramid/tests/fixtures/dummy.ini new file mode 100644 index 000000000..bc2281168 --- /dev/null +++ b/pyramid/tests/fixtures/dummy.ini @@ -0,0 +1,4 @@ +[app:myapp] +use = call:pyramid.tests.test_paster:make_dummyapp + +foo = %(bar)s diff --git a/pyramid/tests/fixtures/static/héhé.html b/pyramid/tests/fixtures/static/héhé.html deleted file mode 100644 index fe5e9af50..000000000 --- a/pyramid/tests/fixtures/static/héhé.html +++ /dev/null @@ -1 +0,0 @@ -<html>hehe file</html> diff --git a/pyramid/tests/fixtures/static/héhé/index.html b/pyramid/tests/fixtures/static/héhé/index.html deleted file mode 100644 index 67623d866..000000000 --- a/pyramid/tests/fixtures/static/héhé/index.html +++ /dev/null @@ -1 +0,0 @@ -<html>hehe</html> diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 123e4f9f5..cfabf9a9d 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -561,9 +561,13 @@ class TestAuthTktCookieHelper(unittest.TestCase): helper.BadTicket = auth_tkt.BadTicket return helper - def _makeRequest(self, cookie=None): + def _makeRequest(self, cookie=None, ipv6=False): environ = {'wsgi.version': (1,0)} - environ['REMOTE_ADDR'] = '1.1.1.1' + + if ipv6 is False: + environ['REMOTE_ADDR'] = '1.1.1.1' + else: + environ['REMOTE_ADDR'] = '::1' environ['SERVER_NAME'] = 'localhost' return DummyRequest(environ, cookie=cookie) @@ -612,6 +616,23 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(environ['REMOTE_USER_DATA'],'') self.assertEqual(environ['AUTH_TYPE'],'cookie') + def test_identify_good_cookie_include_ipv6(self): + helper = self._makeOne('secret', include_ip=True) + request = self._makeRequest('ticket', ipv6=True) + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'userid') + self.assertEqual(result['userdata'], '') + self.assertEqual(result['timestamp'], 0) + self.assertEqual(helper.auth_tkt.value, 'ticket') + self.assertEqual(helper.auth_tkt.remote_addr, '::1') + self.assertEqual(helper.auth_tkt.secret, 'secret') + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + def test_identify_good_cookie_dont_include_ip(self): helper = self._makeOne('secret', include_ip=False) request = self._makeRequest('ticket') @@ -1098,6 +1119,20 @@ class TestAuthTicket(unittest.TestCase): self.assertEqual(result, '66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!') + def test_ipv4(self): + ticket = self._makeOne('secret', 'userid', '198.51.100.1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b'\ + '798400ecdade8d76c530000000auserid!') + + def test_ipv6(self): + ticket = self._makeOne('secret', 'userid', '2001:db8::1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c8'\ + '5becf8760cd7a2fa4910000000auserid!') + class TestBadTicket(unittest.TestCase): def _makeOne(self, msg, expected=None): from pyramid.authentication import BadTicket @@ -1141,6 +1176,19 @@ class Test_parse_ticket(unittest.TestCase): result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + def test_ipv4(self): + ticket = 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecdade8d7'\ + '6c530000000auserid!' + result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + + def test_ipv6(self): + ticket = 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760cd7a2f'\ + 'a4910000000auserid!' + result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + pass + class TestSessionAuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import SessionAuthenticationPolicy diff --git a/pyramid/tests/test_authorization.py b/pyramid/tests/test_authorization.py index 27f2a18b4..60b1b0c8d 100644 --- a/pyramid/tests/test_authorization.py +++ b/pyramid/tests/test_authorization.py @@ -215,6 +215,15 @@ class TestACLAuthorizationPolicy(unittest.TestCase): result = sorted( policy.principals_allowed_by_permission(context, 'read')) self.assertEqual(result, []) + + def test_callable_acl(self): + from pyramid.security import Allow + context = DummyContext() + fn = lambda self: [(Allow, 'bob', 'read')] + context.__acl__ = fn.__get__(context, context.__class__) + policy = self._makeOne() + result = policy.permits(context, ['bob'], 'read') + self.assertTrue(result) class DummyContext: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 4cebdce8a..5388001f6 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3737,6 +3737,13 @@ class TestStaticURLInfo(unittest.TestCase): expected = [('http://example.com/', 'anotherpackage:path/', None)] self._assertRegistrations(config, expected) + def test_add_url_noscheme(self): + inst = self._makeOne() + config = self._makeConfig() + inst.add(config, '//example.com', 'anotherpackage:path') + expected = [('//example.com/', 'anotherpackage:path/', None)] + self._assertRegistrations(config, expected) + def test_add_viewname(self): from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index bf3960b2d..c8418c61d 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -3,7 +3,6 @@ import datetime import locale import os -import platform import unittest from pyramid.wsgi import wsgiapp @@ -82,27 +81,40 @@ class TestStaticAppBase(IntegrationBase): res = self.testapp.get('/static/.hiddenfile', status=200) _assertBody(res.body, os.path.join(here, 'fixtures/static/.hiddenfile')) - if defaultlocale is not None and platform.system() == 'Linux': + if defaultlocale is not None: # These tests are expected to fail on LANG=C systems due to decode # errors and on non-Linux systems due to git highchar handling # vagaries def test_highchars_in_pathelement(self): - url = url_quote('/static/héhé/index.html') - res = self.testapp.get(url, status=200) - _assertBody( - res.body, - os.path.join(here, - text_('fixtures/static/héhé/index.html', 'utf-8')) - ) + path = os.path.join( + here, + text_('fixtures/static/héhé/index.html', 'utf-8')) + pathdir = os.path.dirname(path) + body = b'<html>hehe</html>\n' + try: + os.makedirs(pathdir) + with open(path, 'wb') as fp: + fp.write(body) + url = url_quote('/static/héhé/index.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) + os.rmdir(pathdir) def test_highchars_in_filename(self): - url = url_quote('/static/héhé.html') - res = self.testapp.get(url, status=200) - _assertBody( - res.body, - os.path.join(here, - text_('fixtures/static/héhé.html', 'utf-8')) - ) + path = os.path.join( + here, + text_('fixtures/static/héhé.html', 'utf-8')) + body = b'<html>hehe file</html>\n' + with open(path, 'wb') as fp: + fp.write(body) + try: + url = url_quote('/static/héhé.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) def test_not_modified(self): self.testapp.extra_environ = { @@ -634,6 +646,32 @@ class RendererScanAppTest(IntegrationBase, unittest.TestCase): res = testapp.get('/two', status=200) self.assertTrue(b'Two!' in res.body) +class AcceptContentTypeTest(unittest.TestCase): + def setUp(self): + def hello_view(request): + return {'message': 'Hello!'} + from pyramid.config import Configurator + config = Configurator() + config.add_route('hello', '/hello') + config.add_view(hello_view, route_name='hello', accept='text/plain', renderer='string') + config.add_view(hello_view, route_name='hello', accept='application/json', renderer='json') + app = config.make_wsgi_app() + from webtest import TestApp + self.testapp = TestApp(app) + + def test_ordering(self): + res = self.testapp.get('/hello', headers={'Accept': 'application/json; q=1.0, text/plain; q=0.9'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = self.testapp.get('/hello', headers={'Accept': 'text/plain; q=0.9, application/json; q=1.0'}, status=200) + self.assertEqual(res.content_type, 'application/json') + + def test_wildcards(self): + res = self.testapp.get('/hello', headers={'Accept': 'application/*'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = self.testapp.get('/hello', headers={'Accept': 'text/*'}, status=200) + self.assertEqual(res.content_type, 'text/plain') + + class DummyContext(object): pass @@ -663,4 +701,3 @@ def _assertBody(body, filename): data = data.replace(b'\r', b'') data = data.replace(b'\n', b'') assert(body == data) - diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index b72e0e6b6..5e341172c 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -1,12 +1,12 @@ import os import unittest +here = os.path.dirname(__file__) + class Test_get_app(unittest.TestCase): - def _callFUT(self, config_file, section_name, options=None, loadapp=None): + def _callFUT(self, config_file, section_name, **kw): from pyramid.paster import get_app - return get_app( - config_file, section_name, options=options, loadapp=loadapp - ) + return get_app(config_file, section_name, **kw) def test_it(self): app = DummyApp() @@ -55,15 +55,23 @@ class Test_get_app(unittest.TestCase): self.assertEqual(loadapp.kw, {'global_conf':options}) self.assertEqual(result, app) + def test_it_with_dummyapp_requiring_options(self): + options = {'bar': 'baz'} + app = self._callFUT( + os.path.join(here, 'fixtures', 'dummy.ini'), + 'myapp', options=options) + self.assertEqual(app.settings['foo'], 'baz') + class Test_get_appsettings(unittest.TestCase): - def _callFUT(self, config_file, section_name, appconfig): + def _callFUT(self, config_file, section_name, **kw): from pyramid.paster import get_appsettings - return get_appsettings(config_file, section_name, appconfig) + return get_appsettings(config_file, section_name, **kw) def test_it(self): values = {'a':1} appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini', 'myapp', appconfig) + result = self._callFUT('/foo/bar/myapp.ini', 'myapp', + appconfig=appconfig) self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(appconfig.section_name, 'myapp') self.assertEqual(appconfig.relative_to, os.getcwd()) @@ -72,7 +80,8 @@ class Test_get_appsettings(unittest.TestCase): def test_it_with_hash(self): values = {'a':1} appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini#myapp', None, appconfig) + result = self._callFUT('/foo/bar/myapp.ini#myapp', None, + appconfig=appconfig) self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(appconfig.section_name, 'myapp') self.assertEqual(appconfig.relative_to, os.getcwd()) @@ -81,12 +90,20 @@ class Test_get_appsettings(unittest.TestCase): def test_it_with_hash_and_name_override(self): values = {'a':1} appconfig = DummyLoadWSGI(values) - result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', appconfig) + result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', + appconfig=appconfig) self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(appconfig.section_name, 'yourapp') self.assertEqual(appconfig.relative_to, os.getcwd()) self.assertEqual(result, values) + def test_it_with_dummyapp_requiring_options(self): + options = {'bar': 'baz'} + result = self._callFUT( + os.path.join(here, 'fixtures', 'dummy.ini'), + 'myapp', options=options) + self.assertEqual(result['foo'], 'baz') + class Test_setup_logging(unittest.TestCase): def _callFUT(self, config_file): from pyramid.paster import setup_logging @@ -165,6 +182,12 @@ class DummyApp: def __init__(self): self.registry = dummy_registry +def make_dummyapp(global_conf, **settings): + app = DummyApp() + app.settings = settings + app.global_conf = global_conf + return app + class DummyRequest: application_url = 'http://example.com:5432' script_name = '' @@ -181,6 +204,3 @@ class DummyConfigParser(object): class DummyConfigParserModule(object): ConfigParser = DummyConfigParser - - - diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 65152ca05..432959147 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -1164,6 +1164,104 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) + def test_call_view_raises_predicate_mismatch(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, None) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + + def test_call_view_predicate_mismatch_doesnt_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + from zope.interface import Interface, implementer + class IContext(Interface): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_multiple_predicate_mismatches_dont_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + from zope.interface import Interface, implementer + class IBaseContext(Interface): + pass + class IContext(IBaseContext): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view1 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view1, '', IViewClassifier, IRequest, + DummyContext) + view2 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view2, '', IViewClassifier, IRequest, + IContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, IBaseContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_predicate_mismatch_doesnt_find_unrelated_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + from zope.interface import Interface, implementer + class IContext(Interface): + pass + class IOtherContext(Interface): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + please_dont_call_me_view = DummyView('abc') + self._registerView(self.config.derive_view(please_dont_call_me_view), + '', IViewClassifier, IRequest, IOtherContext) + router = self._makeOne() + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + class DummyPredicate(object): def __call__(self, info, request): return True diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index a7a565356..e33eeebfd 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -583,6 +583,21 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/absstatic/test_url.py') + def test_static_url_noscheme_uses_scheme_from_request(self): + import os + from pyramid.interfaces import IStaticURLInfo + from pyramid.config.views import StaticURLInfo + info = StaticURLInfo() + here = os.path.abspath(os.path.dirname(__file__)) + info.add(self.config, '//subdomain.example.com/static', here) + request = self._makeOne({'wsgi.url_scheme': 'https'}) + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = os.path.join(here, 'test_url.py') + result = request.static_url(abspath) + self.assertEqual(result, + 'https://subdomain.example.com/static/test_url.py') + def test_static_path_abspath(self): from pyramid.interfaces import IStaticURLInfo request = self._makeOne() |
