diff options
279 files changed, 8098 insertions, 3399 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 9859fbf14..c979c4dc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,11 +4,64 @@ Next release Features -------- -- Python 3.2 compatibility (except for Paste scaffolding and paster commands, - which do not work, because Paste has not been ported to Python 3 yet). +- New API: ``pyramid.view.view_defaults``. If you use a class as a view, you + can use the new ``view_defaults`` class decorator on the class to provide + defaults to the view configuration information used by every + ``@view_config`` decorator that decorates a method of that class. It also + works against view configurations involving a class made imperatively. -- Lone instance methods can now be treated as view callables (see - https://github.com/Pylons/pyramid/pull/283). +Documentation +------------- + +- Added documentation to "View Configuration" narrative documentation chapter + about ``view_defaults`` class decorator. + +- Added API docs for ``view_defaults`` class decorator. + +1.3a1 (2011-12-09) +================== + +Features +-------- + +- Python 3.2 compatibility. + +- New ``pyramid.compat`` module and API documentation which provides Python + 2/3 straddling support for Pyramid add-ons and development environments. + +- A ``mako.directories`` setting is no longer required to use Mako templates + Rationale: Mako template renderers can be specified using an absolute asset + spec. An entire application can be written with such asset specs, + requiring no ordered lookup path. + +- ``bpython`` interpreter compatibility in ``pshell``. See the "Command-Line + Pyramid" narrative docs chapter for more information. + +- Added ``get_appsettings`` API function to the ``pyramid.paster`` module. + This function returns the settings defined within an ``[app:...]`` section + in a PasteDeploy ini file. + +- Added ``setup_logging`` API function to the ``pyramid.paster`` module. + This function sets up Python logging according to the logging configuration + in a PasteDeploy ini file. + +- Configuration conflict reporting is reported in a more understandable way + ("Line 11 in file..." vs. a repr of a tuple of similar info). + +- A configuration introspection system was added; see the narrative + documentation chapter entitled "Pyramid Configuration Introspection" for + more information. New APIs: ``pyramid.registry.Introspectable``, + ``pyramid.config.Configurator.introspector``, + ``pyramid.config.Configurator.introspectable``, + ``pyramid.registry.Registry.introspector``. + +- Allow extra keyword arguments to be passed to the + ``pyramid.config.Configurator.action`` method. + +- New APIs: ``pyramid.path.AssetResolver`` and + ``pyramid.path.DottedNameResolver``. The former can be used to resolve + asset specifications, the latter can be used to resolve dotted names to + modules or packages. Bug Fixes --------- @@ -19,11 +72,32 @@ Bug Fixes - The ``pryamid.view.view_config`` decorator did not accept a ``match_params`` predicate argument. See https://github.com/Pylons/pyramid/pull/308 +- The AuthTktCookieHelper could potentially generate Unicode headers + inappropriately when the ``tokens`` argument to remember was used. See + https://github.com/Pylons/pyramid/pull/314. + +- The AuthTktAuthenticationPolicy did not use a timing-attack-aware string + comparator. See https://github.com/Pylons/pyramid/pull/320 for more info. + +- The DummySession in ``pyramid.testing`` now generates a new CSRF token if + one doesn't yet exist. + +- ``request.static_url`` now generates URL-quoted URLs when fed a ``path`` + argument which contains characters that are unsuitable for URLs. See + https://github.com/Pylons/pyramid/issues/349 for more info. + +- Prevent a scaffold rendering from being named ``site`` (conflicts with + Python internal site.py). + +- Support for using instances as targets of the ``pyramid.wsgi.wsgiapp`` and + ``pryramid.wsgi.wsgiapp2`` functions. + See https://github.com/Pylons/pyramid/pull/370 for more info. + Backwards Incompatibilities --------------------------- - Pyramid no longer runs on Python 2.5 (which includes the most recent - release of Jython, and the current version of GAE). + release of Jython and the Python 2.5 version of GAE as of this writing). - The ``paster`` command is no longer the documented way to create projects, start the server, or run debugging commands. To create projects from @@ -39,10 +113,24 @@ Backwards Incompatibilities ``paste.httpserver`` server. Rationale: Rationale: the Paste and PasteScript packages do not run under Python 3. +- The ``pshell`` command (see "paster pshell") no longer accepts a + ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` + or ``--python-shell`` argument, which can be any of the values ``python``, + ``ipython`` or ``bpython``. + +- Removed the ``pyramid.renderers.renderer_from_name`` function. It has been + deprecated since Pyramid 1.0, and was never an API. + +- To use ZCML with versions of Pyramid >= 1.3, you will need ``pyramid_zcml`` + version >= 0.8 and ``zope.configuration`` version >= 3.8.0. The + ``pyramid_zcml`` package version 0.8 is backwards compatible all the way to + Pyramid 1.0, so you won't be warned if you have older versions installed + and upgrade Pyramid "in-place"; it may simply break instead. + Dependencies ------------ -- Pyramid no longer depends on the zope.component package, except as a +- Pyramid no longer depends on the ``zope.component`` package, except as a testing dependency. - Pyramid now depends on a zope.interface>=3.8.0, WebOb>=1.2dev, @@ -52,4 +140,31 @@ Dependencies - Pyramid no longer depends on the Paste or PasteScript packages. +Documentation +------------- + +- The SQLAlchemy Wiki tutorial has been updated. It now uses + ``@view_config`` decorators and an explicit database population script. + +- Minor updates to the ZODB Wiki tutorial. + +- A narrative documentation chapter named "Extending Pyramid Configuration" + was added; it describes how to add a new directive, and how use the + ``pyramid.config.Configurator.action`` method within custom directives. It + also describes how to add introspectable objects. + +- A narrative documentation chapter named "Pyramid Configuration + Introspection" was added. It describes how to query the introspection + system. + +Scaffolds +--------- + +- Rendered scaffolds have now been changed to be more relocatable (fewer + mentions of the package name within files in the package). + +- The ``routesalchemy`` scaffold has been renamed ``alchemy``, replacing the + older (traversal-based) ``alchemy`` scaffold (which has been retired). + +- The ``starter`` scaffold now uses URL dispatch by default. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a368fb4d2..050225b5b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,4 +1,4 @@ -Pylons Project Contributor Agreement + Pylons Project Contributor Agreement ==================================== The submitter agrees by adding his or her name within the section below named @@ -148,3 +148,15 @@ Contributors - Shane Hathaway, 2011/07/22 - Manuel Hermann, 2011/07/11 + +- Richard Barrell, 2011/11/07 + +- Chris Shenton, 2011/11/07 + +- Ken Manheimer, 2011/11/07 + +- Reed O'Brien, 2011/11/07 + +- Klee Dienes, 2011/10/30 + +- Michael Ryabushin, 2011/12/14 diff --git a/HACKING.txt b/HACKING.txt index 0194e9bab..593e89ac1 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -29,11 +29,6 @@ checkout. $ env/bin/easy_install setuptools-git -- Install ``nose`` and ``coverage`` into the virtualenv (for running nose - tests with coverage via ``setup.py nosetests --with-coverage``):: - - $ env/bin/easy_install nose coverage - - Install Pyramid from the checkout into the virtualenv using ``setup.py develop`` (running ``setup.py develop`` *must* be done while the current working directory is the ``pyramid`` checkout directory):: @@ -42,10 +37,10 @@ checkout. $ ../env/bin/python setup.py develop - At that point, you should be able to create new Pyramid projects by using - ``paster create``:: + ``pcreate``:: $ cd ../env - $ bin/paster create -t pyramid_starter starter + $ bin/pcreate -s starter starter - And install those projects (also using ``setup.py develop``) into the virtualenv:: @@ -61,13 +56,15 @@ In order to add a feature to Pyramid: - The feature must be documented in both the API and narrative documentation (in ``docs/``). -- The feature must work fully on the following CPython versions: 2.4, - 2.5, 2.6, and 2.7 on both UNIX and Windows. +- The feature must work fully on the following CPython versions: 2.6, + 2.7, and 3.2 on both UNIX and Windows. + +- The feature must work on the latest version of PyPy. -- The feature must not cause installation or runtime failure on Jython - or App Engine. If it doesn't cause installation or runtime failure, - but doesn't actually *work* on these platforms, that caveat should be - spelled out in the documentation. +- The feature must not cause installation or runtime failure on App Engine. + If it doesn't cause installation or runtime failure, but doesn't actually + *work* on these platforms, that caveat should be spelled out in the + documentation. - The feature must not depend on any particular persistence layer (filesystem, SQL, etc). @@ -76,10 +73,10 @@ In order to add a feature to Pyramid: "unnecessary" is of course subjective, but new dependencies should be discussed). -The above requirements are relaxed for paster template dependencies. -If a paster template has an install-time dependency on something that -doesn't work on a particular platform, that caveat should be spelled -out clearly in *its* documentation (within its ``docs/`` directory). +The above requirements are relaxed for scaffolding dependencies. If a +scaffold has an install-time dependency on something that doesn't work on a +particular platform, that caveat should be spelled out clearly in *its* +documentation (within its ``docs/`` directory). Coding Style ------------ @@ -88,12 +85,33 @@ Coding Style 2 newlines between classes. But 80-column lines, in particular, are mandatory. +Running Tests +-------------- + +- To run tests for Pyramid on a single Python version, run ``python setup.py + test`` against the using the Python interpreter from virtualenv into which + you've ``setup.py develop``-ed Pyramid. + +- To run the full set of Pyramid tests on all platforms, install ``tox`` + (http://codespeak.net/~hpk/tox/) into a system Python. The ``tox`` console + script will be installed into the scripts location for that Python. While + ``cd``'ed to the Pyramid checkout root directory (it contains ``tox.ini``), + invoke the ``tox`` console script. This will read the ``tox.ini`` file and + execute the tests on multiple Python versions and platforms; while it runs, + it creates a virtualenv for each version/platform combination. For + example:: + + $ /usr/bin/easy_install tox + $ cd ~/hack-on-pyramid/pyramid + $ /usr/bin/tox + Test Coverage ------------- -- The codebase *must* have 100% test statement coverage after each - commit. You can test coverage via ``python setup.py nosetests - --with-coverage`` (requires the ``nose`` and ``coverage`` packages). +- The codebase *must* have 100% test statement coverage after each commit. + You can test coverage via ``tox -e coverage``, or alternately by installing + ``nose`` and ``coverage`` into your virtualenv, and running ``setup.py + nosetests --with-coverage``. Documentation Coverage ---------------------- @@ -103,6 +121,16 @@ Documentation Coverage that API or behavior must change to reflect the bug fix, ideally in the same commit that fixes the bug or adds the feature. +- To build and review docs: + + 1. Install ``tests_require`` dependencies from Pyramid's setup.py into your + virtualenv. + + 2. From the ``docs`` directory of the Pyramid checkout run ``make html + SPHINXBUILD=/path/to/your/virtualenv/bin/sphinx-build``. + + 3. Open the _build/html/index.html file to see the resulting rendering. + Change Log ---------- diff --git a/HISTORY.txt b/HISTORY.txt index 956f07362..f6cf8fa87 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -456,7 +456,7 @@ Scaffolds package at all; configuration in the ``production.ini`` file which used to require its ``error_catcher`` middleware has been removed. Configuring error catching / email sending is now the domain of the ``pyramid_exclog`` - package (see https://docs.pylonsproject.org/projects/pyramid_exclog/dev/). + package (see http://docs.pylonsproject.org/projects/pyramid_exclog/dev/). Bug Fixes --------- @@ -1,15 +1,43 @@ Pyramid TODOs ============= +Must-Have +--------- + +- Introspection: + + * Review narrative docs. + + * ``default root factory`` category? + + * ``default view mapper`` category? + + * get rid of "tweens" category (can't sort properly?) + + * implement ptweens and proutes based on introspection instead of current + state of affairs. + + * introspection hiding for directives? + +- Fix deployment recipes in cookbook (discourage proxying without changing + server). + + Nice-to-Have ------------ -- Have ``remember`` and ``forget`` actually set headers on the response using - a response callback (and return the empty list)? +- Implement analogue of "paster request"? -- Add narrative docs for wsgiapp and wsgiapp2. +- CherryPy server testing / exploded from CherryPy itself. + +- Try "with transaction.manager" in an exception view with SQLA (preempt + homina homina response about how to write "to the database" from within in + an exception view). + +- Add a default-view-config-params decorator that can be applied to a class + which names defaults for method-based view_config decorator options. -- Better "Extending" chapter. +- Add narrative docs for wsgiapp and wsgiapp2. - Flesh out "Paste" narrative docs chapter. @@ -62,21 +90,10 @@ Nice-to-Have - Update App engine chapter with less creaky directions. -- Introspection features (whatever is needed by Alan/ptah). - Future ------ -- 1.3: Remove ``pyramid.renderers.renderer_from_name`` (deprecated). - -- 1.3: Kill off ``bfg.routes`` envvars in router. - -- 1.3: Michael's route group work - -- 1.3: Add a default-view-config-params decorator that can be applied to a - class which names defaults for method-based view_config decorator options. - -- 1.3: use zope.registry rather than zope.component. +- 1.4: Kill off ``bfg.routes`` envvars in router. - 1.4: Remove ``chameleon_text`` / ``chameleon_zpt`` deprecated functions (render_*) @@ -109,3 +126,5 @@ Probably Bad Ideas - Supply ``X-Vhm-Host`` support (probably better to do what paste#prefix middleware does). +- Have ``remember`` and ``forget`` actually set headers on the response using + a response callback (and return the empty list)? diff --git a/docs/.gitignore b/docs/.gitignore index 1e9e0413c..da7abd0c0 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,4 @@ -_build _themes +_build + diff --git a/docs/Makefile b/docs/Makefile index 92dc56fda..bb381fc53 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -25,7 +25,7 @@ help: clean: -rm -rf _build/* -html: _themes +html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @@ -47,7 +47,7 @@ pickle: web: pickle -htmlhelp: _themes +htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @@ -84,5 +84,3 @@ epub: @echo @echo "Build finished. The epub file is in _build/epub." -_themes: - git clone git://github.com/Pylons/pylons_sphinx_theme.git _themes diff --git a/docs/api.rst b/docs/api.rst index 6ff6e9fb1..979e8f490 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -12,6 +12,7 @@ documentation is organized alphabetically by module name. api/authentication api/chameleon_text api/chameleon_zpt + api/compat api/config api/events api/exceptions @@ -20,6 +21,7 @@ documentation is organized alphabetically by module name. api/interfaces api/location api/paster + api/path api/registry api/renderers api/request diff --git a/docs/api/compat.rst b/docs/api/compat.rst new file mode 100644 index 000000000..bb34f38e4 --- /dev/null +++ b/docs/api/compat.rst @@ -0,0 +1,156 @@ +.. _compat_module: + +:mod:`pyramid.compat` +---------------------- + +The ``pyramid.compat`` module provides platform and version compatibility for +Pyramid and its add-ons across Python platform and version differences. APIs +will be removed from this module over time as Pyramid ceases to support +systems which require compatibility imports. + +.. automodule:: pyramid.compat + + .. autofunction:: ascii_native_ + + .. attribute:: binary_type + + Binary type for this platform. For Python 3, it's ``bytes``. For + Python 2, it's ``str``. + + .. autofunction:: bytes_ + + .. attribute:: class_types + + Sequence of class types for this platform. For Python 3, it's + ``(type,)``. For Python 2, it's ``(type, types.ClassType)``. + + .. attribute:: configparser + + On Python 2, the ``ConfigParser`` module, on Python 3, the + ``configparser`` module. + + .. function:: escape(v) + + On Python 2, the ``cgi.escape`` function, on Python 3, the + ``html.escape`` function. + + .. function:: exec_(code, globs=None, locs=None) + + Exec code in a compatible way on both Python 2 and 3. + + .. attribute:: im_func + + On Python 2, the string value ``im_func``, on Python 3, the string + value ``__func__``. + + .. function:: input_(v) + + On Python 2, the ``raw_input`` function, on Python 3, the + ``input`` function. + + .. attribute:: integer_types + + Sequence of integer types for this platform. For Python 3, it's + ``(int,)``. For Python 2, it's ``(int, long)``. + + .. function:: is_nonstr_iter(v) + + Return ``True`` if ``v`` is a non-``str`` iterable on both Python 2 and + Python 3. + + .. function:: iteritems_(d) + + Return ``d.items()`` on Python 3, ``d.iteritems()`` on Python 2. + + .. function:: itervalues_(d) + + Return ``d.values()`` on Python 3, ``d.itervalues()`` on Python 2. + + .. function:: iterkeys_(d) + + Return ``d.keys()`` on Python 3, ``d.iterkeys()`` on Python 2. + + .. attribute:: long + + Long type for this platform. For Python 3, it's ``int``. For + Python 2, it's ``long``. + + .. function:: map_(v) + + Return ``list(map(v))`` on Python 3, ``map(v)`` on Python 2. + + .. attribute:: pickle + + ``cPickle`` module if it exists, ``pickle`` module otherwise. + + .. attribute:: PY3 + + ``True`` if running on Python 3, ``False`` otherwise. + + .. attribute:: PYPY + + ``True`` if running on PyPy, ``False`` otherwise. + + .. function:: reraise(tp, value, tb=None) + + Reraise an exception in a compatible way on both Python 2 and Python 3, + e.g. ``reraise(*sys.exc_info())``. + + .. attribute:: string_types + + Sequence of string types for this platform. For Python 3, it's + ``(str,)``. For Python 2, it's ``(basestring,)``. + + .. attribute:: SimpleCookie + + On Python 2, the ``Cookie.SimpleCookie`` class, on Python 3, the + ``http.cookies.SimpleCookie`` module. + + .. autofunction:: text_ + + .. attribute:: text_type + + Text type for this platform. For Python 3, it's ``str``. For Python + 2, it's ``unicode``. + + .. autofunction:: native_ + + .. attribute:: urlparse + + ``urlparse`` module on Python 2, ``urllib.parse`` module on Python 3. + + .. attribute:: url_quote + + ``urllib.quote`` function on Python 2, ``urllib.parse.quote`` function + on Python 3. + + .. attribute:: url_quote_plus + + ``urllib.quote_plus`` function on Python 2, ``urllib.parse.quote_plus`` + function on Python 3. + + .. attribute:: url_unquote + + ``urllib.unquote`` function on Python 2, ``urllib.parse.unquote`` + function on Python 3. + + .. attribute:: url_encode + + ``urllib.urlencode`` function on Python 2, ``urllib.parse.urlencode`` + function on Python 3. + + .. attribute:: url_open + + ``urllib2.urlopen`` function on Python 2, ``urllib.request.urlopen`` + function on Python 3. + + .. function:: url_unquote_text(v, encoding='utf-8', errors='replace') + + On Python 2, return ``url_unquote(v).decode(encoding(encoding, errors))``; + on Python 3, return the result of ``urllib.parse.unquote``. + + .. function:: url_unquote_native(v, encoding='utf-8', errors='replace') + + On Python 2, return ``native_(url_unquote_text_v, encoding, errors))``; + on Python 3, return the result of ``urllib.parse.unquote``. + diff --git a/docs/api/config.rst b/docs/api/config.rst index a8c193b60..dbfbb1761 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,6 +94,23 @@ .. automethod:: set_renderer_globals_factory(factory) + .. attribute:: introspectable + + A shortcut attribute which points to the + :class:`pyramid.registry.Introspectable` class (used during + directives to provide introspection to actions). + + This attribute is new as of :app:`Pyramid` 1.3. + + .. attribute:: introspector + + The :term:`introspector` related to this configuration. It is an + instance implementing the :class:`pyramid.interfaces.IIntrospector` + interface. If the Configurator constructor was supplied with an + ``introspector`` argument, this attribute will be that value. + Otherwise, it will be an instance of a default introspector type. + + This attribute is new as of :app:`Pyramid` 1.3. .. attribute:: global_registries diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index b336e549d..5b190b53b 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -68,3 +68,11 @@ Other Interfaces .. autointerface:: IResponse :members: + .. autointerface:: IIntrospectable + :members: + + .. autointerface:: IIntrospector + :members: + + .. autointerface:: IActionInfo + :members: diff --git a/docs/api/paster.rst b/docs/api/paster.rst index 2a32e07e9..3f7a1c364 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -5,13 +5,10 @@ .. automodule:: pyramid.paster - .. function:: get_app(config_uri, name=None) + .. autofunction:: bootstrap - Return the WSGI application named ``name`` in the PasteDeploy - config file specified by ``config_uri``. + .. autofunction:: get_app(config_uri, name=None) - 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". + .. autofunction:: get_appsettings(config_uri, name=None) - .. autofunction:: bootstrap + .. autofunction:: setup_logging(config_uri) diff --git a/docs/api/path.rst b/docs/api/path.rst new file mode 100644 index 000000000..d46c35d8e --- /dev/null +++ b/docs/api/path.rst @@ -0,0 +1,20 @@ +.. _path_module: + +:mod:`pyramid.path` +--------------------------- + +.. automodule:: pyramid.path + + .. attribute:: CALLER_PACKAGE + + A constant used by the constructor of + :class:`pyramid.path.DottedNameResolver` and + :class:`pyramid.path.AssetResolver` (see their docstrings for more + info). + + .. autoclass:: DottedNameResolver + :members: + + .. autoclass:: AssetResolver + :members: + diff --git a/docs/api/registry.rst b/docs/api/registry.rst index 4d327370a..e18d1b6c2 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -14,3 +14,34 @@ accessed as ``request.registry.settings`` or ``config.registry.settings`` in a typical Pyramid application. + .. attribute:: introspector + + When a registry is set up (or created) by a :term:`Configurator`, the + registry will be decorated with an instance named ``introspector`` + implementing the :class:`pyramid.interfaces.IIntrospector` interface. + See also :attr:`pyramid.config.Configurator.introspector``. + + When a registry is created "by hand", however, this attribute will not + exist until set up by a configurator. + + This attribute is often accessed as ``request.registry.introspector`` in + a typical Pyramid application. + + This attribute is new as of :app:`Pyramid` 1.3. + +.. class:: Introspectable + + The default implementation of the interface + :class:`pyramid.interfaces.IIntrospectable` used by framework exenders. + An instance of this class is is created when + :attr:`pyramid.config.Configurator.introspectable` is called. + + This class is new as of :app:`Pyramid` 1.3. + +.. class:: noop_introspector + + An introspector which throws away all registrations, useful for disabling + introspection altogether (pass as ``introspector`` to the + :term:`Configurator` constructor). + + This class is new as of :app:`Pyramid` 1.3. diff --git a/docs/api/settings.rst b/docs/api/settings.rst index ac1cd3f9c..6b12c038c 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -9,4 +9,6 @@ .. autofunction:: asbool + .. autofunction:: aslist + diff --git a/docs/api/view.rst b/docs/api/view.rst index 4dddea25f..9f59ddae7 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -16,6 +16,9 @@ .. autoclass:: view_config :members: + .. autoclass:: view_defaults + :members: + .. autoclass:: static :members: :inherited-members: diff --git a/docs/conf.py b/docs/conf.py index c78285840..9be5db325 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,20 +39,6 @@ LaTeXTranslator.depart_inline = nothing book = os.environ.get('BOOK') -# If your extensions are in another directory, add it here. If the directory -# is relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -parent = os.path.dirname(os.path.dirname(__file__)) -sys.path.append(os.path.abspath(parent)) -wd = os.getcwd() -os.chdir(parent) -os.system('%s setup.py test -q' % sys.executable) -os.chdir(wd) - -for item in os.listdir(parent): - if item.endswith('.egg'): - sys.path.append(os.path.join(parent, item)) - # General configuration # --------------------- @@ -94,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.2' +version = '1.4dev' # The full version, including alpha/beta/rc tags. release = version @@ -141,14 +127,42 @@ if book: # ----------------------- # Add and use Pylons theme -sys.path.append(os.path.abspath('_themes')) +if 'sphinx-build' in ' '.join(sys.argv): # protect against dumb importers + from subprocess import call, Popen, PIPE + + p = Popen('which git', shell=True, stdout=PIPE) + git = p.stdout.read().strip() + cwd = os.getcwd() + _themes = os.path.join(cwd, '_themes') + + if not os.path.isdir(_themes): + call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git', + '_themes']) + else: + os.chdir(_themes) + call([git, 'checkout', 'master']) + call([git, 'pull']) + os.chdir(cwd) + + sys.path.append(os.path.abspath('_themes')) + + parent = os.path.dirname(os.path.dirname(__file__)) + sys.path.append(os.path.abspath(parent)) + wd = os.getcwd() + os.chdir(parent) + os.system('%s setup.py test -q' % sys.executable) + os.chdir(wd) + + for item in os.listdir(parent): + if item.endswith('.egg'): + sys.path.append(os.path.join(parent, item)) + html_theme_path = ['_themes'] html_theme = 'pyramid' - -html_theme_options = { - 'github_url': 'https://github.com/Pylons/pyramid' -} - +html_theme_options = dict( + github_url='https://github.com/Pylons/pyramid', +# in_progress='true' + ) # 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. @@ -461,7 +475,7 @@ def resig(app, what, name, obj, options, signature, return_annotation): # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = 'The Pyramid Web Application Development Framework, Version 1.2' +epub_title = 'The Pyramid Web Application Development Framework, Version 1.4dev' epub_author = 'Chris McDonough' epub_publisher = 'Agendaless Consulting' epub_copyright = '2008-2011' @@ -478,7 +492,7 @@ epub_scheme = 'ISBN' epub_identifier = '0615445675' # A unique identification for the text. -epub_uid = 'The Pyramid Web Application Development Framework, Version 1.2' +epub_uid = 'The Pyramid Web Application Development Framework, Version 1.4dev' # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. diff --git a/docs/glossary.rst b/docs/glossary.rst index fc282b2da..e4de15bd6 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -342,14 +342,13 @@ Glossary file. It was developed by Ian Bicking. Chameleon - `chameleon <http://chameleon.repoze.org>`_ is an attribute - language template compiler which supports both the :term:`ZPT` and - :term:`Genshi` templating specifications. It is written and - maintained by Malthe Borch. It has several extensions, such as - the ability to use bracketed (Genshi-style) ``${name}`` syntax, - even within ZPT. It is also much faster than the reference - implementations of both ZPT and Genshi. :app:`Pyramid` offers - Chameleon templating out of the box in ZPT and text flavors. + `chameleon <http://chameleon.repoze.org>`_ is an attribute language + template compiler which supports the :term:`ZPT` templating + specification. It is written and maintained by Malthe Borch. It has + several extensions, such as the ability to use bracketed (Mako-style) + ``${name}`` syntax. It is also much faster than the reference + implementation of ZPT. :app:`Pyramid` offers Chameleon templating out + of the box in ZPT and text flavors. ZPT The `Zope Page Template <http://wiki.zope.org/ZPT/FrontPage>`_ @@ -564,9 +563,8 @@ Glossary also `PEP 318 <http://www.python.org/dev/peps/pep-0318/>`_. configuration declaration - An individual method call made to an instance of a :app:`Pyramid` - :term:`Configurator` object which performs an arbitrary action, such as - registering a :term:`view configuration` (via the + An individual method call made to a :term:`configuration directive`, + such as registering a :term:`view configuration` (via the :meth:`~pyramid.config.Configurator.add_view` method of the configurator) or :term:`route configuration` (via the :meth:`~pyramid.config.Configurator.add_route` method of the @@ -922,7 +920,7 @@ Glossary database information. :mod:`pyramid_debugtoolbar` is configured into the ``development.ini`` of all applications which use a Pyramid :term:`scaffold`. For more information, see - https://docs.pylonsproject.org/projects/pyramid_debugtoolbar/dev/ . + http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/dev/ . scaffold A project template that helps users get started writing a Pyramid @@ -935,5 +933,53 @@ Glossary used in production applications, because the logger can be configured to log to a file, to UNIX syslog, to the Windows Event Log, or even to email. See its `documentation - <https://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. - + <http://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. + + console script + A script written to the ``bin`` (on UNIX, or ``Scripts`` on Windows) + directory of a Python installation or virtualenv as the result of + running ``setup.py install`` or ``setup.py develop``. + + introspector + An object with the methods described by + :class:`pyramid.interfaces.IIntrospector` that is available in both + configuration code (for registration) and at runtime (for querying) that + allows a developer to introspect configuration statements and + relationships between those statements. + + conflict resolution + Pyramid attempts to resolve ambiguous configuration statements made by + application developers via automatic conflict resolution. Automatic + conflict resolution is described in + :ref:`automatic_conflict_resolution`. If Pyramid cannot resolve + ambiguous configuration statements, it is possible to manually resolve + them as described in :ref:`manually_resolving_conflicts`. + + configuration directive + A method of the :term:`Configurator` which causes a configuration action + to occur. The method :meth:`pyramid.config.Configurator.add_view` is a + configuration directive, and application developers can add their own + directives as necessary (see :ref:`add_directive`). + + action + Represents a pending configuration statement generated by a call to a + :term:`configuration directive`. The set of pending configuration + actions are processed when :meth:`pyramid.config.Configurator.commit` is + called. + + discriminator + The unique identifier of an :term:`action`. + + introspectable + An object which implements the attributes and methods described in + :class:`pyramid.interfaces.IIntrospectable`. Introspectables are used + by the :term:`introspector` to display configuration information about + a running Pyramid application. An introspectable is associated with a + :term:`action` by virtue of the + :meth:`pyramid.config.Configurator.action` method. + + asset descriptor + An instance representing an :term:`asset specification` provided by the + :meth:`pyramid.path.AssetResolver.resolve` method. It supports the + methods and attributes documented in + :class:`pyramid.interfaces.IAssetDescriptor`. diff --git a/docs/index.rst b/docs/index.rst index e4de8b0c8..df7a422d4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ Front Matter .. toctree:: :maxdepth: 1 + whatsnew-1.3 whatsnew-1.2 whatsnew-1.1 whatsnew-1.0 @@ -65,6 +66,7 @@ Narrative documentation in chapter form explaining how to use narr/configuration narr/project narr/startup + narr/router narr/urldispatch narr/views narr/renderers @@ -87,9 +89,10 @@ Narrative documentation in chapter form explaining how to use narr/security narr/hybrid narr/hooks - narr/advconfig + narr/introspector narr/extending - narr/router + narr/advconfig + narr/extconfig narr/threadlocals narr/zca diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 584dd3825..4db5b64b2 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -31,6 +31,8 @@ Narrative Documentation narr/configuration narr/firstapp narr/project + narr/startup + narr/router narr/urldispatch narr/views narr/renderers @@ -53,9 +55,10 @@ Narrative Documentation narr/security narr/hybrid narr/hooks - narr/advconfig + narr/introspector narr/extending - narr/startup + narr/advconfig + narr/extconfig narr/threadlocals narr/zca diff --git a/docs/narr/MyProject/README.txt b/docs/narr/MyProject/README.txt index 5e10949fc..c28d0d94a 100644 --- a/docs/narr/MyProject/README.txt +++ b/docs/narr/MyProject/README.txt @@ -1,4 +1 @@ MyProject README - - - diff --git a/docs/narr/MyProject/development.ini b/docs/narr/MyProject/development.ini index 3a4758c44..d61da580f 100644 --- a/docs/narr/MyProject/development.ini +++ b/docs/narr/MyProject/development.ini @@ -41,6 +41,6 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s # End logging configuration diff --git a/docs/narr/MyProject/myproject/__init__.py b/docs/narr/MyProject/myproject/__init__.py index 04e219e36..31b02cf02 100644 --- a/docs/narr/MyProject/myproject/__init__.py +++ b/docs/narr/MyProject/myproject/__init__.py @@ -1,12 +1,10 @@ from pyramid.config import Configurator -from myproject.resources import Root def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - config = Configurator(root_factory=Root, settings=settings) - config.add_view('myproject.views.my_view', - context='myproject.resources.Root', - renderer='myproject:templates/mytemplate.pt') - config.add_static_view('static', 'myproject:static') + config = Configurator(settings=settings) + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() return config.make_wsgi_app() diff --git a/docs/narr/MyProject/myproject/resources.py b/docs/narr/MyProject/myproject/resources.py deleted file mode 100644 index 3d811895c..000000000 --- a/docs/narr/MyProject/myproject/resources.py +++ /dev/null @@ -1,3 +0,0 @@ -class Root(object): - def __init__(self, request): - self.request = request diff --git a/docs/narr/MyProject/myproject/static/pylons.css b/docs/narr/MyProject/myproject/static/pylons.css index 33b21ac1a..c54499ddd 100644 --- a/docs/narr/MyProject/myproject/static/pylons.css +++ b/docs/narr/MyProject/myproject/static/pylons.css @@ -23,7 +23,7 @@ h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} +body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "NobileRegular","Lucida Grande",Lucida,Verdana,sans-serif;} a{color:#1b61d6;text-decoration:none;} a:hover{color:#e88f00;text-decoration:underline;} body h1, @@ -31,19 +31,20 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +body h6{font-family:"NeutonRegular","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} #wrap{min-height:100%;} #header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} #header{background:#000000;top:0;font-size:14px;} #footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} .header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} -#top,#bottom{width:100%;} -#top{color:#000000;height:230px; -background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#top,#top-small,#bottom{width:100%;} +#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} #bottom{color:#222;background-color:#ffffff;} -.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} .top{padding-top:40px;} +.top-small{padding-top:10px;} #middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} @@ -58,7 +59,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{width:205px;} +input[type=text],input[type=password]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/pyramid-small.png b/docs/narr/MyProject/myproject/static/pyramid-small.png Binary files differindex a5bc0ade7..a5bc0ade7 100644 --- a/pyramid/scaffolds/routesalchemy/+package+/static/pyramid-small.png +++ b/docs/narr/MyProject/myproject/static/pyramid-small.png diff --git a/docs/narr/MyProject/myproject/templates/mytemplate.pt b/docs/narr/MyProject/myproject/templates/mytemplate.pt index 97f1e1aa3..0bfac946e 100644 --- a/docs/narr/MyProject/myproject/templates/mytemplate.pt +++ b/docs/narr/MyProject/myproject/templates/mytemplate.pt @@ -1,42 +1,29 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" - xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> <head> <title>The Pyramid Web Application Development Framework</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('myproject:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('myproject:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" - href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" - type="text/css" media="screen" charset="utf-8" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('myproject:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div> - <img src="${request.static_url('myproject:static/pyramid.png')}" - width="750" height="169" alt="pyramid"/> - </div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> <div class="middle align-center"> <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, - an application generated by<br/> + Welcome to <span class="app-name">${project}</span>, an application generated by<br/> the Pyramid web application development framework. </p> </div> @@ -45,62 +32,45 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" - action="http://docs.pylonsproject.org/pyramid/current/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html"> + <input type="text" id="q" name="q" value="" /> + <input type="submit" id="x" value="Go" /> + </form> </div> <div id="right" class="align-left"> <h2>Pyramid links</h2> <ul class="links"> <li> - <a href="http://pylonsproject.org"> - Pylons Website - </a> + <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation"> - Narrative Documentation - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation"> - API Documentation - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials"> - Tutorials - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history"> - Change History - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications"> - Sample Applications - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development"> - Support and Development - </a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> </li> <li> - <a href="irc://irc.freenode.net#pyramid"> - IRC Channel - </a> + <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> </li> - </ul> + </ul> </div> </div> </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2010, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py index 5fa710278..d8b764041 100644 --- a/docs/narr/MyProject/myproject/tests.py +++ b/docs/narr/MyProject/myproject/tests.py @@ -10,9 +10,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from myproject.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'MyProject') - - diff --git a/docs/narr/MyProject/myproject/views.py b/docs/narr/MyProject/myproject/views.py index c43b34460..f571a5976 100644 --- a/docs/narr/MyProject/myproject/views.py +++ b/docs/narr/MyProject/myproject/views.py @@ -1,2 +1,5 @@ +from pyramid.view import view_config + +@view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): return {'project':'MyProject'} diff --git a/docs/narr/MyProject/production.ini b/docs/narr/MyProject/production.ini index 9d025715d..97050e8fe 100644 --- a/docs/narr/MyProject/production.ini +++ b/docs/narr/MyProject/production.ini @@ -25,11 +25,11 @@ keys = console keys = generic [logger_root] -level = INFO +level = WARN handlers = console [logger_myproject] -level = INFO +level = WARN handlers = qualname = myproject diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 7b62b1a73..3a7bf2805 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -87,8 +87,8 @@ that ends something like this: Conflicting configuration actions For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, None, None, None, None, None, False, None, None, None) - ('app.py', 14, '<module>', 'config.add_view(hello_world)') - ('app.py', 17, '<module>', 'config.add_view(hello_world)') + Line 14 of file app.py in <module>: 'config.add_view(hello_world)' + Line 17 of file app.py in <module>: 'config.add_view(goodbye_world)' This traceback is trying to tell us: @@ -115,6 +115,8 @@ Conflict detection happens for any kind of configuration: imperative configuration or configuration that results from the execution of a :term:`scan`. +.. _manually_resolving_conflicts: + Manually Resolving Conflicts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -397,76 +399,3 @@ constraints: the routes they imply require relative ordering. Such ordering constraints are not absolved by two-phase configuration. Routes are still added in configuration execution order. -.. index:: - single: add_directive - pair: configurator; adding directives - -.. _add_directive: - -Adding Methods to the Configurator via ``add_directive`` --------------------------------------------------------- - -Framework extension writers can add arbitrary methods to a -:term:`Configurator` by using the -:meth:`pyramid.config.Configurator.add_directive` method of the configurator. -This makes it possible to extend a Pyramid configurator in arbitrary ways, -and allows it to perform application-specific tasks more succinctly. - -The :meth:`~pyramid.config.Configurator.add_directive` method accepts two -positional arguments: a method name and a callable object. The callable -object is usually a function that takes the configurator instance as its -first argument and accepts other arbitrary positional and keyword arguments. -For example: - -.. code-block:: python - :linenos: - - from pyramid.events import NewRequest - from pyramid.config import Configurator - - def add_newrequest_subscriber(config, subscriber): - config.add_subscriber(subscriber, NewRequest). - - if __name__ == '__main__': - config = Configurator() - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can -then call the method by its given name as if it were a built-in method of the -Configurator: - -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - config.add_newrequest_subscriber(mysubscriber) - -A call to :meth:`~pyramid.config.Configurator.add_directive` is often -"hidden" within an ``includeme`` function within a "frameworky" package meant -to be included as per :ref:`including_configuration` via -:meth:`~pyramid.config.Configurator.include`. For example, if you put this -code in a package named ``pyramid_subscriberhelpers``: - -.. code-block:: python - :linenos: - - def includeme(config) - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -The user of the add-on package ``pyramid_subscriberhelpers`` would then be -able to install it and subsequently do: - -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - from pyramid.config import Configurator - config = Configurator() - config.include('pyramid_subscriberhelpers') - config.add_newrequest_subscriber(mysubscriber) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 0dc41e919..66ef46671 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -111,6 +111,7 @@ For a URL that doesn't match any views, ``pviews`` will simply print out a single: interactive shell single: IPython single: pshell + single: bpython .. _interactive_shell: @@ -267,23 +268,26 @@ exposed, and the request is configured to generate urls from the host .. index:: single: IPython + single: bpython -IPython -~~~~~~~ +.. _ipython_or_bpython: + +IPython or bpython +~~~~~~~~~~~~~~~~~~ -If you have `IPython <http://en.wikipedia.org/wiki/IPython>`_ installed in -the interpreter you use to invoke the ``pshell`` command, ``pshell`` will use -an IPython interactive shell instead of a standard Python interpreter shell. -If you don't want this to happen, even if you have IPython installed, you can -pass the ``--disable-ipython`` flag to the ``pshell`` command to use a -standard Python interpreter shell unconditionally. +If you have `IPython <http://en.wikipedia.org/wiki/IPython>`_ or +`bpython <http://bpython-interpreter.org/>`_ or both installed in +the interpreter you use to invoke the ``pshell`` command, ``pshell`` will +autodiscover them and use the first respectively found in this order : +IPython, bpython, standard Python interpreter. However you could +specifically invoke one of your choice with the ``-p choice`` or +``--python-shell choice`` option. .. code-block:: text - [chrism@vitaminf shellenv]$ ../bin/pshell --disable-ipython \ + [chrism@vitaminf shellenv]$ ../bin/pshell -p ipython | bpython | python \ development.ini#MyProject - .. index:: pair: routes; printing single: proutes diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst new file mode 100644 index 000000000..5e7fe2753 --- /dev/null +++ b/docs/narr/extconfig.rst @@ -0,0 +1,364 @@ +.. index:: + single: extending configuration + +.. _extconfig_narr: + +Extending Pyramid Configuration +=============================== + +Pyramid allows you to extend its Configurator with custom directives. Custom +directives can use other directives, they can add a custom :term:`action`, +they can participate in :term:`conflict resolution`, and they can provide +some number of :term:`introspectable` objects. + +.. index:: + single: add_directive + pair: configurator; adding directives + +.. _add_directive: + +Adding Methods to the Configurator via ``add_directive`` +-------------------------------------------------------- + +Framework extension writers can add arbitrary methods to a +:term:`Configurator` by using the +:meth:`pyramid.config.Configurator.add_directive` method of the configurator. +Using :meth:`~pyramid.config.Configurator.add_directive` makes it possible to +extend a Pyramid configurator in arbitrary ways, and allows it to perform +application-specific tasks more succinctly. + +The :meth:`~pyramid.config.Configurator.add_directive` method accepts two +positional arguments: a method name and a callable object. The callable +object is usually a function that takes the configurator instance as its +first argument and accepts other arbitrary positional and keyword arguments. +For example: + +.. code-block:: python + :linenos: + + from pyramid.events import NewRequest + from pyramid.config import Configurator + + def add_newrequest_subscriber(config, subscriber): + config.add_subscriber(subscriber, NewRequest) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can +then call the added directive by its given name as if it were a built-in +method of the Configurator: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print event.request + + config.add_newrequest_subscriber(mysubscriber) + +A call to :meth:`~pyramid.config.Configurator.add_directive` is often +"hidden" within an ``includeme`` function within a "frameworky" package meant +to be included as per :ref:`including_configuration` via +:meth:`~pyramid.config.Configurator.include`. For example, if you put this +code in a package named ``pyramid_subscriberhelpers``: + +.. code-block:: python + :linenos: + + def includeme(config): + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +The user of the add-on package ``pyramid_subscriberhelpers`` would then be +able to install it and subsequently do: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print event.request + + from pyramid.config import Configurator + config = Configurator() + config.include('pyramid_subscriberhelpers') + config.add_newrequest_subscriber(mysubscriber) + +Using ``config.action`` in a Directive +-------------------------------------- + +If a custom directive can't do its work exclusively in terms of existing +configurator methods (such as +:meth:`pyramid.config.Configurator.add_subscriber`, as above), the directive +may need to make use of the :meth:`pyramid.config.Configurator.action` +method. This method adds an entry to the list of "actions" that Pyramid will +attempt to process when :meth:`pyramid.config.Configurator.commit` is called. +An action is simply a dictionary that includes a :term:`discriminator`, +possibly a callback function, and possibly other metadata used by Pyramid's +action system. + +Here's an example directive which uses the "action" method: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(): + config.registry.jammyjam = jammyjam + config.action('jammyjam', register) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +Fancy, but what does it do? The action method accepts a number of arguments. +In the above directive named ``add_jammyjam``, we call +:meth:`~pyramid.config.Configurator.action` with two arguments: the string +``jammyjam`` is passed as the first argument named ``discriminator``, and the +closure function named ``register`` is passed as the second argument named +``callable``. + +When the :meth:`~pyramid.config.Configurator.action` method is called, it +appends an action to the list of pending configuration actions. All pending +actions with the same discriminator value are potentially in conflict with +one another (see :ref:`conflict_detection`). When the +:meth:`~pyramid.config.Configurator.commit` method of the Configurator is +called (either explicitly or as the result of calling +:meth:`~pyramid.config.Configurator.make_wsgi_app`), conflicting actions are +potentially automatically resolved as per +:ref:`automatic_conflict_resolution`. If a conflict cannot be automatically +resolved, a :exc:`ConfigurationConflictError` is raised and application +startup is prevented. + +In our above example, therefore, if a consumer of our ``add_jammyjam`` +directive did this: + +.. code-block:: python + + config.add_jammyjam('first') + config.add_jammyjam('second') + +When the action list was committed resulting from the set of calls above, our +user's application would not start, because the discriminators of the actions +generated by the two calls are in direct conflict. Automatic conflict +resolution cannot resolve the conflict (because no ``config.include`` is +involved), and the user provided no intermediate +:meth:`pyramid.config.Configurator.commit` call between the calls to +``add_jammyjam`` to ensure that the successive calls did not conflict with +each other. + +This demonstrates the purpose of the discriminator argument to the action +method: it's used to indicate a uniqueness constraint for an action. Two +actions with the same discriminator will conflict unless the conflict is +automatically or manually resolved. A discriminator can be any hashable +object, but it is generally a string or a tuple. *You use a discriminator to +declaratively ensure that the user doesn't provide ambiguous configuration +statements.* + +But let's imagine that a consumer of ``add_jammyjam`` used it in such a way +that no configuration conflicts are generated. + +.. code-block:: python + + config.add_jammyjam('first') + +What happens now? When the ``add_jammyjam`` method is called, an action is +appended to the pending actions list. When the pending configuration actions +are processed during :meth:`~pyramid.config.Configurator.commit`, and no +conflicts occur, the *callable* provided as the second argument to the +:meth:`~pyramid.config.Configurator.action` method within ``add_jammyjam`` is +called with no arguments. The callable in ``add_jammyjam`` is the +``register`` closure function. It simply sets the value +``config.registry.jammyjam`` to whatever the user passed in as the +``jammyjam`` argument to the ``add_jammyjam`` function. Therefore, the +result of the user's call to our directive will set the ``jammyjam`` +attribute of the registry to the string ``first``. *A callable is used by a +directive to defer the result of a user's call to the directive until +conflict detection has had a chance to do its job*. + +Other arguments exist to the :meth:`~pyramid.config.Configurator.action` +method, including ``args``, ``kw``, ``order``, and ``introspectables``. + +``args`` and ``kw`` exist as values, which, if passed, will be used as +arguments to the ``callable`` function when it is called back. For example +our directive might use them like so: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(*arg, **kw): + config.registry.jammyjam_args = arg + config.registry.jammyjam_kw = kw + config.registry.jammyjam = jammyjam + config.action('jammyjam', register, args=('one',), kw={'two':'two'}) + +In the above example, when this directive is used to generate an action, and +that action is committed, ``config.registry.jammyjam_args`` will be set to +``('one',)`` and ``config.registry.jammyjam_kw`` will be set to +``{'two':'two'}``. ``args`` and ``kw`` are honestly not very useful when +your ``callable`` is a closure function, because you already usually have +access to every local in the directive without needing them to be passed +back. They can be useful, however, if you don't use a closure as a callable. + +``order`` is a crude order control mechanism. ``order`` defaults to the +integer ``0``; it can be set to any other integer. All actions that share an +order will be called before other actions that share a higher order. This +makes it possible to write a directive with callable logic that relies on the +execution of the callable of another directive being done first. For +example, Pyramid's :meth:`pyramid.config.Configurator.add_view` directive +registers an action with a higher order than the +:meth:`pyramid.config.Configurator.add_route` method. Due to this, the +``add_view`` method's callable can assume that, if a ``route_name`` was +passed to it, that a route by this name was already registered by +``add_route``, and if such a route has not already been registered, it's a +configuration error (a view that names a nonexistent route via its +``route_name`` parameter will never be called). + +``introspectables`` is a sequence of :term:`introspectable` objects. You can +pass a sequence of introspectables to the +:meth:`~pyramid.config.Configurator.action` method, which allows you to +augment Pyramid's configuration introspection system. + +.. _introspection: + +Adding Configuration Introspection +---------------------------------- + +.. note:: + + The introspection subsystem is new in Pyramid 1.3. + +Pyramid provides a configuration introspection system that can be used by +debugging tools to provide visibility into the configuration of a running +application. + +All built-in Pyramid directives (such as +:meth:`pyramid.config.Configurator.add_view` and +:meth:`pyramid.config.Configurator.add_route`) register a set of +introspectables when called. For example, when you register a view via +``add_view``, the directive registers at least one introspectable: an +introspectable about the view registration itself, providing human-consumable +values for the arguments it was passed. You can later use the introspection +query system to determine whether a particular view uses a renderer, or +whether a particular view is limited to a particular request method, or which +routes a particular view is registered against. The Pyramid "debug toolbar" +makes use of the introspection system in various ways to display information +to Pyramid developers. + +Introspection values are set when a sequence of :term:`introspectable` +objects is passed to the :meth:`~pyramid.config.Configurator.action` method. +Here's an example of a directive which uses introspectables: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value): + def register(): + config.registry.jammyjam = value + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + config.action('jammyjam', register, introspectables=(intr,)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +If you notice, the above directive uses the ``introspectable`` attribute of a +Configurator (:attr:`pyramid.config.Configurator.introspectable`) to create +an introspectable object. The introspectable object's constructor requires +at least four arguments: the ``category_name``, the ``discriminator``, the +``title``, and the ``type_name``. + +The ``category_name`` is a string representing the logical category for this +introspectable. Usually the category_name is a pluralization of the type of +object being added via the action. + +The ``discriminator`` is a value unique **within the category** (unlike the +action discriminator, which must be unique within the entire set of actions). +It is typically a string or tuple representing the values unique to this +introspectable within the category. It is used to generate links and as part +of a relationship-forming target for other introspectables. + +The ``title`` is a human-consumable string that can be used by introspection +system frontends to show a friendly summary of this introspectable. + +The ``type_name`` is a value that can be used to subtype this introspectable +within its category for for sorting and presentation purposes. It can be any +value. + +An introspectable is also dictionary-like. It can contain any set of +key/value pairs, typically related to the arguments passed to its related +directive. While the category_name, discriminator, title and type_name are +*metadata* about the introspectable, the values provided as key/value pairs +are the actual data provided by the introspectable. In the above example, we +set the ``value`` key to the value of the ``value`` argument passed to the +directive. + +Our directive above mutates the introspectable, and passes it in to the +``action`` method as the first element of a tuple as the value of the +``introspectable`` keyword argument. This associates this introspectable +with the action. Introspection tools will then display this introspectable +in their index. + +Introspectable Relationships +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two introspectables may have relationships between each other. + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value, template): + def register(): + config.registry.jammyjam = (value, template) + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + tmpl_intr = config.introspectable(category_name='jammyjam templates', + discriminator=template, + title=template, + type_name=None) + tmpl_intr['value'] = template + intr.relate('jammyjam templates', template) + config.action('jammyjam', register, introspectables=(intr, tmpl_intr)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +In the above example, the ``add_jammyjam`` directive registers two +introspectables. The first is related to the ``value`` passed to the +directive; the second is related to the ``template`` passed to the directive. +If you believe a concept within a directive is important enough to have its +own introspectable, you can cause the same directive to register more than +one introspectable, registering one introspectable for the "main idea" and +another for a related concept. + +The call to ``intr.relate`` above +(:meth:`pyramid.interfaces.IIntrospectable.relate`) is passed two arguments: +a category name and a directive. The example above effectively indicates +that the directive wishes to form a relationship between the ``intr`` +introspectable and the ``tmpl_intr`` introspectable; the arguments passed to +``relate`` are the category name and discriminator of the ``tmpl_intr`` +introspectable. + +Relationships need not be made between two introspectables created by the +same directive. Instead, a relationship can be formed between an +introspectable created in one directive and another introspectable created in +another by calling ``relate`` on either side with the other directive's +category name and discriminator. An error will be raised at configuration +commit time if you attempt to relate an introspectable with another +nonexistent introspectable, however. + +Introspectable relationships will show up in frontend system renderings of +introspection values. For example, if a view registration names a route +name, the introspectable related to the view callable will show a reference +to the route to which it relates to and vice versa. diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index 9c96248f2..c464203f0 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -200,8 +200,8 @@ like this: overridden elements, such as templates and static assets as necessary. - Install the new package into the same Python environment as the original - application (e.g. ``python setup.py develop`` or ``python setup.py - install``). + application (e.g. ``$myvenv/bin/python setup.py develop`` or + ``$myvenv/bin/python setup.py install``). - Change the ``main`` function in the new package's ``__init__.py`` to include the original :app:`Pyramid` application's configuration functions via diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index 45d65402c..c082f616b 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -71,11 +71,10 @@ named ``hello_world``. :linenos: :pyobject: hello_world -This function doesn't do anything very difficult. The functions accepts a -single argument (``request``). The ``hello_world`` function returns an -instance of the :class:`pyramid.response.Response`. The single argument to -the class' constructor is value computed from arguments matched from the url -route. This value becomes the body of the response. +The function accepts a single argument (``request``) and it returns an +instance of the :class:`pyramid.response.Response` class. The single +argument to the class' constructor is a string computed from parameters +matched from the URL. This value becomes the body of the response. This function is known as a :term:`view callable`. A view callable accepts a single argument, ``request``. It is expected to return a @@ -134,7 +133,7 @@ Using the ``if`` clause is necessary -- or at least best practice -- because code in a Python ``.py`` file may be eventually imported via the Python ``import`` statement by another ``.py`` file. ``.py`` files that are imported by other ``.py`` files are referred to as *modules*. By using the -``if __name__ == 'main':`` idiom, the script above is indicating that it does +``if __name__ == '__main__':`` idiom, the script above is indicating that it does not want the code within the ``if`` statement to execute if this module is imported from another; the code within the ``if`` block should only be run during a direct script execution. @@ -157,7 +156,7 @@ Adding Configuration :lines: 10-11 First line above calls the :meth:`pyramid.config.Configurator.add_route` -method, which registers a :term:`route` to match any url path that begins +method, which registers a :term:`route` to match any URL path that begins with ``/hello/`` followed by a string. The second line, ``config.add_view(hello_world, route_name='hello')``, @@ -208,7 +207,7 @@ WSGI Application Serving Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :func:`paste.httpserver.serve` WSGI server runner, passing it the ``app`` object (a :term:`router`) as the application -we wish to serve. We also pass in an argument ``host=='0.0.0.0'``, meaning +we wish to serve. We also pass in an argument ``host='0.0.0.0'``, meaning "listen on all TCP interfaces." By default, the HTTP server listens only on the ``127.0.0.1`` interface, which is problematic if you're running the server on a remote system and you wish to access it with a web browser diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py index 93a403a13..7c26c8cdc 100644 --- a/docs/narr/helloworld.py +++ b/docs/narr/helloworld.py @@ -10,6 +10,6 @@ if __name__ == '__main__': config.add_route('hello', '/hello/{name}') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() - server = make_server('0.0.0.0', 8080) + server = make_server('0.0.0.0', 8080, app) server.serve_forever() diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index c2ecba9bb..e261f9a11 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -149,7 +149,7 @@ generated by using it. For example: from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('pyramid') - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}) + ts = _('add-number', default='Add ${number}', mapping={'number':1}) .. note:: We assigned the translation string factory to the name ``_``. This is a convention which will be supported by translation @@ -170,7 +170,7 @@ to: :linenos: from pyramid.i18n import TranslationString as _ - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}, + ts = _('add-number', default='Add ${number}', mapping={'number':1}, domain='pyramid') You can set up your own translation string factory much like the one @@ -185,7 +185,7 @@ do something like this: from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('form') - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}) + ts = _('add-number', default='Add ${number}', mapping={'number':1}) Creating a unique domain for your application via a translation string factory is best practice. Using your own unique translation domain @@ -249,7 +249,7 @@ GNU gettext uses three types of files in the translation framework, The tools for working with :term:`gettext` translation files related to a :app:`Pyramid` application is :term:`Babel` and :term:`Lingua`. Lingua is a -Balel extension that provides support for scraping i18n references out of +Babel extension that provides support for scraping i18n references out of Python and Chameleon files. .. index:: @@ -347,7 +347,7 @@ extract the messages: $ cd /place/where/myapplication/setup.py/lives $ mkdir -p myapplication/locale - $ python setup.py extract_messages + $ $myvenv/bin/python setup.py extract_messages The message catalog ``.pot`` template will end up in: @@ -439,7 +439,7 @@ init_catalog`` command: .. code-block:: text $ cd /place/where/myapplication/setup.py/lives - $ python setup.py init_catalog -l es + $ $myvenv/bin/python setup.py init_catalog -l es By default, the message catalog ``.po`` file will end up in: @@ -471,7 +471,7 @@ Then use the ``setup.py update_catalog`` command. .. code-block:: text $ cd /place/where/myapplication/setup.py/lives - $ python setup.py update_catalog + $ $myvenv/bin/python setup.py update_catalog .. index:: pair: compiling; message catalog @@ -487,7 +487,7 @@ translations, compile ``.po`` files to ``.mo`` files: .. code-block:: text $ cd /place/where/myapplication/setup.py/lives - $ python setup.py compile_catalog + $ $myvenv/bin/python setup.py compile_catalog This will create a ``.mo`` file for each ``.po`` file in your application. As long as the :term:`translation directory` in which diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 66bcea706..3de4d6e27 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -9,19 +9,18 @@ Installing :app:`Pyramid` Before You Install ------------------ -You will need `Python <http://python.org>`_ version 2.5 or better to +You will need `Python <http://python.org>`_ version 2.6 or better to run :app:`Pyramid`. .. sidebar:: Python Versions - As of this writing, :app:`Pyramid` has been tested under Python 2.5.5, - Python 2.6.6, and Python 2.7.2. :app:`Pyramid` does not run under any - version of Python before 2.5, and does not yet run under Python 3.X. + As of this writing, :app:`Pyramid` has been tested under Python 2.6.6, + Python 2.7.2, and Python 3.2. :app:`Pyramid` does not 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 Google's App Engine, :term:`PyPy` (1.5 and 1.6), and -:term:`Jython` (2.5.2). +known to run on Google's App Engine, and :term:`PyPy` (1.6+). :app:`Pyramid` installation does not require the compilation of any C code, so you need only a Python interpreter that meets the @@ -325,25 +324,6 @@ Installing :app:`Pyramid` on Google App Engine :ref:`appengine_tutorial` documents the steps required to install a :app:`Pyramid` application on Google App Engine. -.. index:: - single: installing on Jython - -Installing :app:`Pyramid` on Jython --------------------------------------- - -:app:`Pyramid` is known to work under :term:`Jython` version 2.5.1. -Install :term:`Jython`, and then follow the installation steps for -:app:`Pyramid` on your platform described in one of the sections -entitled :ref:`installing_unix` or :ref:`installing_windows` above, -replacing the ``python`` command with ``jython`` as necessary. The -steps are exactly the same except you should use the ``jython`` -command name instead of the ``python`` command name. - -One caveat exists to using :app:`Pyramid` under Jython: the :term:`Chameleon` -templating engine does not work on Jython. However, the :term:`Mako` -templating system, which is also included with Pyramid, does work under -Jython; use it instead. - What Gets Installed ------------------- diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 547f88ef3..7c6ad00f3 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -219,7 +219,7 @@ that the Pyramid core doesn't. Add-on packages already exist which let you easily send email, let you use the Jinja2 templating system, let you use XML-RPC or JSON-RPC, let you integrate with jQuery Mobile, etc. -Examples: https://docs.pylonsproject.org/docs/pyramid.html#pyramid-add-on-documentation +Examples: http://docs.pylonsproject.org/docs/pyramid.html#pyramid-add-on-documentation Class-based and function-based views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -808,11 +808,11 @@ Every release of Pyramid has 100% statement coverage via unit and integration tests, as measured by the ``coverage`` tool available on PyPI. It also has greater than 95% decision/condition coverage as measured by the ``instrumental`` tool available on PyPI. It is automatically tested by the -Jenkins tool on Python 2.5, Python 2.6, Python 2.7, Jython and PyPy after -each commit to its GitHub repository. Official Pyramid add-ons are held to a -similar testing standard. We still find bugs in Pyramid and its official -add-ons, but we've noticed we find a lot more of them while working on other -projects that don't have a good testing regime. +Jenkins tool on Python 2.6, Python 2.7, Python 3.2 and PyPy after each commit +to its GitHub repository. Official Pyramid add-ons are held to a similar +testing standard. We still find bugs in Pyramid and its official add-ons, +but we've noticed we find a lot more of them while working on other projects +that don't have a good testing regime. Example: http://jenkins.pylonsproject.org/ @@ -842,7 +842,7 @@ official narrative docs. In any case, the Pyramid documentation is comprehensive. Example: The rest of this documentation and the cookbook at -https://docs.pylonsproject.org/projects/pyramid_cookbook/dev/ . +http://docs.pylonsproject.org/projects/pyramid_cookbook/dev/ . .. index:: single: Pylons Project diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst new file mode 100644 index 000000000..11d779854 --- /dev/null +++ b/docs/narr/introspector.rst @@ -0,0 +1,557 @@ +.. index:: + single: introspection + single: introspector + +.. _using_introspection: + +Pyramid Configuration Introspection +=================================== + +When Pyramid starts up, each call to a :term:`configuration directive` causes +one or more :term:`introspectable` objects to be registered with an +:term:`introspector`. The introspector can be queried by application code to +obtain information about the configuration of the running application. This +feature is useful for debug toolbars, command-line scripts which show some +aspect of configuration, and for runtime reporting of startup-time +configuration settings. + +.. warning:: + + Introspection is new in Pyramid 1.3. + +Using the Introspector +---------------------- + +Here's an example of using Pyramid's introspector from within a view +callable: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='bar') + def route_accepts(request): + introspector = request.registry.introspector + route_name = request.matched_route.name + route_intr = introspector.get('routes', route_name) + return Response(str(route_intr['pattern'])) + +This view will return a response that contains the "pattern" argument +provided to the ``add_route`` method of the route which matched when the view +was called. It uses the :meth:`pyramid.interfaces.IIntrospector.get` method +to return an introspectable in the category ``routes`` with a +:term:`discriminator` equal to the matched route name. It then uses the +returned introspectable to obtain a "pattern" value. + +The introspectable returned by the query methods of the introspector has +methods and attributes described by +:class:`pyramid.interfaces.IIntrospectable`. In particular, the +:meth:`~pyramid.interfaces.IIntrospector.get`, +:meth:`~pyramid.interfaces.IIntrospector.get_category`, +:meth:`~pyramid.interfaces.IIntrospector.categories`, +:meth:`~pyramid.interfaces.IIntrospector.categorized`, and +:meth:`~pyramid.interfaces.IIntrospector.related` methods of an introspector +can be used to query for introspectables. + +Introspectable Objects +---------------------- + +Introspectable objects are returned from query methods of an introspector. +Each introspectable object implements the attributes and methods +documented at :class:`pyramid.interfaces.IIntrospectable`. + +The important attributes shared by all introspectables are the following: + +``title`` + + A human-readable text title describing the introspectable + +``category_name`` + + A text category name describing the introspection category to which this + introspectable belongs. It is often a plural if there are expected to be + more than one introspectable registered within the category. + +``discriminator`` + + A hashable object representing the unique value of this introspectable + within its category. + +``discriminator_hash`` + + The integer hash of the discriminator (useful for using in HTML links). + +``type_name`` + + The text name of a subtype within this introspectable's category. If there + is only one type name in this introspectable's category, this value will + often be a singular version of the category name but it can be an arbitrary + value. + +``action_info`` + + An object describing the directive call site which caused this + introspectable to be registered; contains attributes described in + :class:`pyramid.interfaces.IActionInfo`. + +Besides having the attributes described above, an introspectable is a +dictionary-like object. An introspectable can be queried for data values via +its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods. +For example: + +.. code-block:: python + :linenos: + + route_intr = introspector.get('routes', 'edit_user') + pattern = route_intr['pattern'] + +Pyramid Introspection Categories +-------------------------------- + +The list of concrete introspection categories provided by built-in Pyramid +configuration directives follows. Add-on packages may supply other +introspectables in categories not described here. + +``subscribers`` + + Each introspectable in the ``subscribers`` category represents a call to + :meth:`pyramid.config.Configurator.add_subscriber` (or the decorator + equivalent); each will have the following data. + + ``subscriber`` + + The subscriber callable object (the resolution of the ``subscriber`` + argument passed to ``add_susbcriber``). + + ``interfaces`` + + A sequence of interfaces (or classes) that are subscribed to (the + resolution of the ``ifaces`` argument passed to ``add_subscriber``). + +``response adapters`` + + Each introspectable in the ``response adapters`` category represents a call + to :meth:`pyramid.config.Configurator.add_response_adapter` (or a decorator + equivalent); each will have the following data. + + ``adapter`` + + The adapter object (the resolved ``adapter`` argument to + ``add_response_adapter``). + + ``type`` + + The resolved ``type_or_iface`` argument passed to + ``add_response_adapter``. + +``root factories`` + + Each introspectable in the ``root factories`` category represents a call to + :meth:`pyramid.config.Configurator.set_root_factory` (or the Configurator + constructor equivalent) *or* a ``factory`` argument passed to + :meth:`pyramid.config.Configurator.add_route`; each will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_root_factory``). + + ``route_name`` + + The name of the route which will use this factory. If this is the + *default* root factory (if it's registered during a call to + ``set_root_factory``), this value will be ``None``. + +``session factory`` + + Only one introspectable will exist in the ``session factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_session_factory` + (or the Configurator constructor equivalent); it will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_session_factory``). + +``request factory`` + + Only one introspectable will exist in the ``request factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_request_factory` + (or the Configurator constructor equivalent); it will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_request_factory``). + +``locale negotiator`` + + Only one introspectable will exist in the ``locale negotiator`` category. + It represents a call to + :meth:`pyramid.config.Configurator.set_locale_negotiator` (or the + Configurator constructor equivalent); it will have the following data. + + ``negotiator`` + + The factory object (the resolved ``negotiator`` argument to + ``set_locale_negotiator``). + +``renderer factories`` + + Each introspectable in the ``renderer factories`` category represents a + call to :meth:`pyramid.config.Configurator.add_renderer` (or the + Configurator constructor equivalent); each will have the following data. + + ``name`` + + The name of the renderer (the value of the ``name`` argument to + ``add_renderer``). + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``add_renderer``). + +``renderer globals factory`` + + There will be one and only one introspectable in the ``renderer globals + factory`` category. It represents a call to + :meth:`pyramid.config.Configurator.set_renderer_globals_factory`; it will + have the following data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_renderer_globals_factory``). + +``routes`` + + Each introspectable in the ``routes`` category represents a call to + :meth:`pyramid.config.Configurator.add_route`; each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_route``. + + ``pattern`` + + The ``pattern`` argument passed to ``add_route``. + + ``factory`` + + The (resolved) ``factory`` argument passed to ``add_route``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_route``. + + ``request_method`` + + The ``request_method`` argument passed to ``add_route``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_route`` or the value ``None`` if a + ``request_method`` argument was not supplied. + + ``path_info`` + + The ``path_info`` argument passed to ``add_route``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_route``. + + ``header`` + + The ``header`` argument passed to ``add_route``. + + ``accept`` + + The ``accept`` argument passed to ``add_route``. + + ``traverse`` + + The ``traverse`` argument passed to ``add_route``. + + ``custom_predicates`` + + The ``custom_predicates`` argument passed to ``add_route``. + + ``pregenerator`` + + The ``pregenerator`` argument passed to ``add_route``. + + ``static`` + + The ``static`` argument passed to ``add_route``. + + ``use_global_views`` + + The ``use_global_views`` argument passed to ``add_route``. + + ``object`` + + The :class:`pyramid.interfaces.IRoute` object that is used to perform + matching and generation for this route. + +``authentication policy`` + + There will be one and only one introspectable in the ``authentication + policy`` category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authentication_policy` method (or + its Configurator constructor equivalent); it will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authentication_policy``). + +``authorization policy`` + + There will be one and only one introspectable in the ``authorization + policy`` category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authorization_policy` method (or its + Configurator constructor equivalent); it will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authorization_policy``). + +``default permission`` + + There will be one and only one introspectable in the ``default permission`` + category. It represents a call to the + :meth:`pyramid.config.Configurator.set_default_permission` method (or its + Configurator constructor equivalent); it will have the following data. + + ``value`` + + The permission name passed to ``set_default_permission``. + +``views`` + + Each introspectable in the ``views`` category represents a call to + :meth:`pyramid.config.Configurator.add_view`; each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_view``. + + ``context`` + + The (resolved) ``context`` argument passed to ``add_view``. + + ``containment`` + + The (resolved) ``containment`` argument passed to ``add_view``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_view``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_view`` or the value ``None`` if a + ``request_method`` argument was not supplied. + + ``route_name`` + + The ``route_name`` argument passed to ``add_view``. + + ``attr`` + + The ``attr`` argument passed to ``add_view``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_view``. + + ``accept`` + + The ``accept`` argument passed to ``add_view``. + + ``header`` + + The ``header`` argument passed to ``add_view``. + + ``path_info`` + + The ``path_info`` argument passed to ``add_view``. + + ``match_param`` + + The ``match_param`` argument passed to ``add_view``. + + ``callable`` + + The (resolved) ``view`` argument passed to ``add_view``. Represents the + "raw" view callable. + + ``derived_callable`` + + The view callable derived from the ``view`` argument passed to + ``add_view``. Represents the view callable which Pyramid itself calls + (wrapped in security and other wrappers). + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view``. + + ``decorator`` + + The (resolved) ``decorator`` argument passed to ``add_view``. + +``permissions`` + + Each introspectable in the ``permissions`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit + ``permission`` argument to *or* a call to + :meth:`pyramid.config.Configurator.set_default_permission`; each will have + the following data. + + ``value`` + + The permission name passed to ``add_view`` or ``set_default_permission``. + +``templates`` + + Each introspectable in the ``templates`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has a ``renderer`` + argument which points to a template; each will have the following data. + + ``name`` + + The renderer's name (a string). + + ``type`` + + The renderer's type (a string). + + ``renderer`` + + The :class:`pyramid.interfaces.IRendererInfo` object which represents + this template's renderer. + +``view mapper`` + + Each introspectable in the ``permissions`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit + ``mapper`` argument to *or* a call to + :meth:`pyramid.config.Configurator.set_view_mapper`; each will have + the following data. + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view`` or + ``set_view_mapper``. + +``asset overrides`` + + Each introspectable in the ``asset overrides`` category represents a call + to :meth:`pyramid.config.Configurator.override_asset`; each will have the + following data. + + ``to_override`` + + The ``to_override`` argument (an asset spec) passed to + ``override_asset``. + + ``override_with`` + + The ``override_with`` argument (an asset spec) passed to + ``override_asset``. + +``translation directories`` + + Each introspectable in the ``asset overrides`` category represents an + individual element in a ``specs`` argument passed to to + :meth:`pyramid.config.Configurator.add_translation_dirs`; each will have + the following data. + + ``directory`` + + The absolute path of the translation directory. + + ``spec`` + + The asset specification passed to ``add_translation_dirs``. + +``tweens`` + + Each introspectable in the ``tweens`` category represents a call to + :meth:`pyramid.config.Configurator.add_tween`; each will have the following + data. + + ``name`` + + The dotted name to the tween factory as a string (passed as + the ``tween_factory`` argument to ``add_tween``). + + ``factory`` + + The (resolved) tween factory object. + + ``type`` + + ``implict`` or ``explicit`` as a string. + + ``under`` + + The ``under`` argument passed to ``add_tween`` (a string). + + ``over`` + + The ``over`` argument passed to ``add_tween`` (a string). + +``static views`` + + Each introspectable in the ``static views`` category represents a call to + :meth:`pyramid.config.Configurator.add_static_view`; each will have the + following data. + + ``name`` + + The ``name`` argument provided to ``add_static_view``. + + ``spec`` + + A normalized version of the ``spec`` argument provided to + ``add_static_view``. + +Introspection in the Toolbar +---------------------------- + +The Pyramid debug toolbar (part of the ``pyramid_debugtoolbar`` package) +provides a canned view of all registered introspectables and their +relationships. It looks something like this: + +.. image:: tb_introspector.png + +Disabling Introspection +----------------------- + +You can disable Pyramid introspection by passing the object +:attr:`pyramid.registry.noop_introspector` to the :term:`Configurator` +constructor in your application setup: + +.. code-block:: python + + from pyramid.config import Configurator + from pyramid.registry import noop_introspector + config = Configurator(..., introspector=noop_introspector) + +When the noop introspector is active, all introspectables generated by +configuration directives are thrown away. A noop introspector behaves just +like a "real" introspector, but the methods of a noop introspector do nothing +and return null values. diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 5377d0c66..044655c1f 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -16,6 +16,8 @@ how to send log messages to loggers that you've configured. a third-party scaffold which does not create these files, the configuration information in this chapter will not be applicable. +.. _logging_config: + Logging Configuration --------------------- @@ -291,7 +293,7 @@ Logging Exceptions To log (or email) exceptions generated by your :app:`Pyramid` application, use the :term:`pyramid_exclog` package. Details about its configuration are in its `documentation -<https://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. +<http://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. Request Logging with Paste's TransLogger ---------------------------------------- diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index a948e57cc..4a249ed0d 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -4,7 +4,9 @@ Much Ado About Traversal ======================== -.. note:: This chapter was adapted, with permission, from a blog post by `Rob +.. note:: + + This chapter was adapted, with permission, from a blog post by `Rob Miller <http://blog.nonsequitarian.org/>`_, originally published at http://blog.nonsequitarian.org/2010/much-ado-about-traversal/ . diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 4f96448af..af8714573 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -28,7 +28,6 @@ as part of Pyramid. single: starter scaffold single: zodb scaffold single: alchemy scaffold - single: routesalchemy scaffold .. _additional_paster_scaffolds: @@ -47,27 +46,17 @@ each other on a number of axes: The included scaffolds are these: ``starter`` - URL mapping via :term:`traversal` and no persistence mechanism. + URL mapping via :term:`URL dispatch` and no persistence mechanism. ``zodb`` - URL mapping via :term:`traversal` and persistence via :term:`ZODB`. - -``routesalchemy`` - URL mapping via :term:`URL dispatch` and persistence via - :term:`SQLAlchemy` + URL mapping via :term:`traversal` and persistence via :term:`ZODB`. *Note + that, as of this writing, this scaffold will not run under Python 3, only + under Python 2.* ``alchemy`` - URL mapping via :term:`traversal` and persistence via + URL mapping via :term:`URL dispatch` and persistence via :term:`SQLAlchemy` -.. note:: - -Rather than use any of the above scaffolds, Pylons 1 users may feel more -comfortable installing the :term:`Akhet` development environment, which -provides a scaffold named ``akhet``. This scaffold configures a Pyramid -application in a "Pylons-esque" way, including the use of a :term:`view -handler` to map URLs to code (a handler is much like a Pylons "controller"). - .. index:: single: creating a project single: project @@ -98,18 +87,18 @@ Or on Windows: The above command uses the ``pcreate`` command to create a project with the ``starter`` scaffold. To use a different scaffold, such as -``routesalchemy``, you'd just change the ``-s`` argument value. For example, +``alchemy``, you'd just change the ``-s`` argument value. For example, on UNIX: .. code-block:: text - $ bin/pcreate -s routesalchemy MyProject + $ bin/pcreate -s alchemy MyProject Or on Windows: .. code-block:: text - $ Scripts\pcreate routesalchemy MyProject + $ Scripts\pcreate alchemy MyProject Here's sample output from a run of ``pcreate`` on UNIX for a project we name ``MyProject``: @@ -386,7 +375,6 @@ structure: |-- MANIFEST.in |-- myproject | |-- __init__.py - | |-- resources.py | |-- static | | |-- favicon.ico | | |-- logo.png @@ -686,8 +674,6 @@ The ``myproject`` :term:`package` lives inside the ``MyProject`` ``main`` function which is used as a entry point for commands such as ``pserve``, ``pshell``, ``pviews``, and others. -#. A ``resources.py`` module, which contains :term:`resource` code. - #. A ``templates`` directory, which contains :term:`Chameleon` (or other types of) templates. @@ -723,34 +709,25 @@ also informs Python that the directory which contains it is a *package*. #. Line 1 imports the :term:`Configurator` class from :mod:`pyramid.config` that we use later. -#. Line 2 imports the ``Root`` class from :mod:`myproject.resources` that we - use later. - -#. Lines 4-12 define a function named ``main`` that returns a :app:`Pyramid` +#. Lines 3-16 define a function named ``main`` that returns a :app:`Pyramid` WSGI application. This function is meant to be called by the :term:`PasteDeploy` framework as a result of running ``pserve``. Within this function, application configuration is performed. - Lines 8-10 register a "default view" (a view that has no ``name`` - attribute). It is registered so that it will be found when the - :term:`context` of the request is an instance of the - :class:`myproject.resources.Root` class. The first argument to - ``add_view`` points at a Python function that does all the work for this - view, also known as a :term:`view callable`, via a :term:`dotted Python - name`. The view declaration also names a ``renderer``, which in this case - is a template that will be used to render the result of the view callable. - This particular view declaration points at - ``myproject:templates/mytemplate.pt``, which is a :term:`asset - specification` that specifies the ``mytemplate.pt`` file within the - ``templates`` directory of the ``myproject`` package. The template file - it actually points to is a :term:`Chameleon` ZPT template file. - - Line 11 registers a static view, which will serve up the files from the + Line 6 creates an instance of a :term:`Configurator`. + + Line 7 registers a static view, which will serve up the files from the ``mypackage:static`` :term:`asset specification` (the ``static`` directory of the ``mypackage`` package). - Line 12 returns a :term:`WSGI` application to the caller of the function + Line 8 adds a :term:`route` to the configuration. This route is later + used by a view in the ``views`` module. + + Line 9 calls ``config.scan()``, which picks up view registrations declared + elsewhere in the package (in this case, in the ``views.py`` module). + + Line 10 returns a :term:`WSGI` application to the caller of the function (Pyramid's pserve). .. index:: @@ -768,10 +745,22 @@ and which returns a :term:`response`. :language: python :linenos: -This bit of code was registered as the view callable within ``__init__.py`` -(via ``add_view``). ``add_view`` said that the default URL for instances -that are of the class :class:`myproject.resources.Root` should run this -:func:`myproject.views.my_view` function. +Lines 3-5 define and register a :term:`view callable` named ``my_view``. The +function named ``my_view`` is decorated with a ``view_config`` decorator +(which is processed by the ``config.scan()`` line in our ``__init__.py``). +The view_config decorator asserts that this view be found when a +:term:`route` named ``home`` is matched. In our case, because our +``__init__.py`` maps the route named ``home`` to the URL pattern ``/``, this +route will match when a visitor visits the root URL. The view_config +decorator also names a ``renderer``, which in this case is a template that +will be used to render the result of the view callable. This particular view +declaration points at ``templates/mytemplate.pt``, which is a :term:`asset +specification` that specifies the ``mytemplate.pt`` file within the +``templates`` directory of the ``myproject`` package. The asset +specification could have also been specified as +``myproject:templates/mytemplate.pt``; the leading package name and colon is +optional. The template file it actually points to is a :term:`Chameleon` ZPT +template file. This view callable function is handed a single piece of information: the :term:`request`. The *request* is an instance of the :term:`WebOb` @@ -781,8 +770,7 @@ This view returns a dictionary. When this view is invoked, a :term:`renderer` converts the dictionary returned by the view into HTML, and returns the result as the :term:`response`. This view is configured to invoke a renderer which uses a :term:`Chameleon` ZPT template -(``mypackage:templates/my_template.pt``, as specified in the ``__init__.py`` -file call to ``add_view``). +(``templates/my_template.pt``). See :ref:`views_which_use_a_renderer` for more information about how views, renderers, and templates relate and cooperate. @@ -798,35 +786,6 @@ renderers, and templates relate and cooperate. the speed at which templates may be rendered. .. index:: - single: resources.py - -.. _resourcespy_project_section: - -``resources.py`` -~~~~~~~~~~~~~~~~ - -The ``resources.py`` module provides the :term:`resource` data and behavior -for our application. Resources are objects which exist to provide site -structure in applications which use :term:`traversal` to map URLs to code. -We write a class named ``Root`` that provides the behavior for the root -resource. - -.. literalinclude:: MyProject/myproject/resources.py - :language: python - :linenos: - -#. Lines 1-3 define the Root class. The Root class is a "root resource - factory" function that will be called by the :app:`Pyramid` *Router* for - each request when it wants to find the root of the resource tree. - -In a "real" application, the Root object would likely not be such a simple -object. Instead, it might be an object that could access some persistent -data store, such as a database. :app:`Pyramid` doesn't make any assumption -about which sort of data storage you'll want to use, so the sample -application uses an instance of :class:`myproject.resources.Root` to -represent the root. - -.. index:: single: static directory ``static`` @@ -838,11 +797,11 @@ template. It includes CSS and images. ``templates/mytemplate.pt`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The single :term:`Chameleon` template that exists in the project. Its contents -are too long to show here, but it displays a default page when rendered. It -is referenced by the call to ``add_view`` as the ``renderer`` attribute in -the ``__init__`` file. See :ref:`views_which_use_a_renderer` for more -information about renderers. +The single :term:`Chameleon` template that exists in the project. Its +contents are too long to show here, but it displays a default page when +rendered. It is referenced by the call to ``@view_config`` as the +``renderer`` of the ``my_view`` view callable in the ``views.py`` file. See +:ref:`views_which_use_a_renderer` for more information about renderers. Templates are accessed and used by view configurations and sometimes by view functions themselves. See :ref:`templates_used_directly` and @@ -907,39 +866,12 @@ named ``views`` instead of within a single ``views.py`` file, you might: can be empty, this just tells Python that the ``views`` directory is a *package*. -Then change the __init__.py of your myproject project (*not* the -``__init__.py`` you just created in the ``views`` directory, the one in its -parent directory). For example, from something like: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.my_view', - renderer='myproject:templates/mytemplate.pt') - -To this: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.blog.my_view', - renderer='myproject:templates/mytemplate.pt') - -You can then continue to add files to the ``views`` directory, and refer to -view classes or functions within those files via the dotted name passed as -the first argument to ``add_view``. For example, if you added a file named -``anothermodule.py`` to the ``views`` subdirectory, and added a view callable -named ``my_view`` to it: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.anothermodule.my_view', - renderer='myproject:templates/anothertemplate.pt') - -This pattern can be used to rearrage code referred to by any Pyramid API -argument which accepts a :term:`dotted Python name` or direct object -reference. +You can then continue to add view callable functions to the ``blog.py`` +module, but you can also add other ``.py`` files which contain view callable +functions to the ``views`` directory. As long as you use the +``@view_config`` directive to register views in conjuction with +``config.scan()`` they will be picked up automatically when the application +is restarted. Using the Interactive Shell --------------------------- @@ -949,16 +881,39 @@ configuration as would be loaded if you were running your Pyramid application via ``pserve``. This can be a useful debugging tool. See :ref:`interactive_shell` for more details. +.. _alternate_wsgi_server: + Using an Alternate WSGI Server ------------------------------ -The code generated by a :app:`Pyramid` scaffold assumes that you will be +The code generated by :app:`Pyramid` scaffolding assumes that you will be using the ``pserve`` command to start your application while you do -development. However, ``pserve`` is by no means the only way to start up and -serve a :app:`Pyramid` application. As we saw in :ref:`firstapp_chapter`, -``pserve`` needn't be invoked at all to run a :app:`Pyramid` application. -The use of ``pserve`` to run a :app:`Pyramid` application is purely -conventional based on the output of its scaffold. +development. The default rendering of Pyramid scaffolding uses the *wsgiref* +WSGI server, which is a server that is ill-suited for production usage: its +main feature is that it works on all platforms and all systems, making it a +good choice as a default server from the perspective of Pyramid's developers. + +To use a server more suitable for production, you have a number of choices. +Replace the ``use = egg:pyramid#wsgref`` line in your ``production.ini`` with +one of the following. + +``use = egg:Paste#http`` + + ``paste.httpserver`` is Windows, UNIX, and Python 2 compatible. You'll + need to ``easy_install Paste`` into your Pyramid virtualenv for this server + to work. + +``use = egg:pyramid#cherrypy`` + + The ``CherryPy`` WSGI server is Windows, UNIX, Python 2, and Python 3 + compatible. You'll need to ``easy_install CherryPy`` into your Pyramid + virtualenv for this server to work. + +``pserve`` is by no means the only way to start up and serve a :app:`Pyramid` +application. As we saw in :ref:`firstapp_chapter`, ``pserve`` needn't be +invoked at all to run a :app:`Pyramid` application. The use of ``pserve`` to +run a :app:`Pyramid` application is purely conventional based on the output +of its scaffold. Any :term:`WSGI` server is capable of running a :app:`Pyramid` application. Some WSGI servers don't require the :term:`PasteDeploy` framework's diff --git a/docs/narr/router.rst b/docs/narr/router.rst index d08261b17..b78362066 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -9,74 +9,67 @@ Request Processing ================== -Once a :app:`Pyramid` application is up and running, it is ready to -accept requests and return responses. +Once a :app:`Pyramid` application is up and running, it is ready to accept +requests and return responses. What happens from the time a :term:`WSGI` +request enters a :app:`Pyramid` application through to the point that +:app:`Pyramid` hands off a response back to WSGI for upstream processing? -What happens from the time a :term:`WSGI` request enters a -:app:`Pyramid` application through to the point that -:app:`Pyramid` hands off a response back to WSGI for upstream -processing? +#. A user initiates a request from his browser to the hostname and port + number of the WSGI server used by the :app:`Pyramid` application. -#. A user initiates a request from his browser to the hostname and - port number of the WSGI server used by the :app:`Pyramid` - application. - -#. The WSGI server used by the :app:`Pyramid` application passes - the WSGI environment to the ``__call__`` method of the - :app:`Pyramid` :term:`router` object. +#. The WSGI server used by the :app:`Pyramid` application passes the WSGI + environment to the ``__call__`` method of the :app:`Pyramid` + :term:`router` object. #. A :term:`request` object is created based on the WSGI environment. -#. The :term:`application registry` and the :term:`request` object - created in the last step are pushed on to the :term:`thread local` - stack that :app:`Pyramid` uses to allow the functions named +#. The :term:`application registry` and the :term:`request` object created in + the last step are pushed on to the :term:`thread local` stack that + :app:`Pyramid` uses to allow the functions named :func:`~pyramid.threadlocal.get_current_request` and :func:`~pyramid.threadlocal.get_current_registry` to work. #. A :class:`~pyramid.events.NewRequest` :term:`event` is sent to any subscribers. -#. If any :term:`route` has been defined within application - configuration, the :app:`Pyramid` :term:`router` calls a - :term:`URL dispatch` "route mapper." The job of the mapper is to - examine the request to determine whether any user-defined - :term:`route` matches the current WSGI environment. The - :term:`router` passes the request as an argument to the mapper. - -#. If any route matches, the request is mutated; a ``matchdict`` and - ``matched_route`` attributes are added to the request object; the - former contains a dictionary representing the matched dynamic - elements of the request's ``PATH_INFO`` value, the latter contains - the :class:`~pyramid.interfaces.IRoute` object representing the - route which matched. The root object associated with the route - found is also generated: if the :term:`route configuration` which - matched has an associated a ``factory`` argument, this factory is - used to generate the root object, otherwise a default :term:`root - factory` is used. - -#. If a route match was *not* found, and a ``root_factory`` argument - was passed to the :term:`Configurator` constructor, that callable - is used to generate the root object. If the ``root_factory`` - argument passed to the Configurator constructor was ``None``, a - default root factory is used to generate a root object. - -#. The :app:`Pyramid` router calls a "traverser" function with the - root object and the request. The traverser function attempts to - traverse the root object (using any existing ``__getitem__`` on the - root object and subobjects) to find a :term:`context`. If the root - object has no ``__getitem__`` method, the root itself is assumed to - be the context. The exact traversal algorithm is described in - :ref:`traversal_chapter`. The traverser function returns a - dictionary, which contains a :term:`context` and a :term:`view - name` as well as other ancillary information. - -#. The request is decorated with various names returned from the - traverser (such as ``context``, ``view_name``, and so forth), so - they can be accessed via e.g. ``request.context`` within - :term:`view` code. - -#. A :class:`~pyramid.events.ContextFound` :term:`event` is - sent to any subscribers. +#. If any :term:`route` has been defined within application configuration, + the :app:`Pyramid` :term:`router` calls a :term:`URL dispatch` "route + mapper." The job of the mapper is to examine the request to determine + whether any user-defined :term:`route` matches the current WSGI + environment. The :term:`router` passes the request as an argument to the + mapper. + +#. If any route matches, the route mapper adds attributes to the request: + ``matchdict`` and ``matched_route`` attributes are added to the request + object. The former contains a dictionary representing the matched dynamic + elements of the request's ``PATH_INFO`` value, the latter contains the + :class:`~pyramid.interfaces.IRoute` object representing the route which + matched. The root object associated with the route found is also + generated: if the :term:`route configuration` which matched has an + associated a ``factory`` argument, this factory is used to generate the + root object, otherwise a default :term:`root factory` is used. + +#. If a route match was *not* found, and a ``root_factory`` argument was + passed to the :term:`Configurator` constructor, that callable is used to + generate the root object. If the ``root_factory`` argument passed to the + Configurator constructor was ``None``, a default root factory is used to + generate a root object. + +#. The :app:`Pyramid` router calls a "traverser" function with the root + object and the request. The traverser function attempts to traverse the + root object (using any existing ``__getitem__`` on the root object and + subobjects) to find a :term:`context`. If the root object has no + ``__getitem__`` method, the root itself is assumed to be the context. The + exact traversal algorithm is described in :ref:`traversal_chapter`. The + traverser function returns a dictionary, which contains a :term:`context` + and a :term:`view name` as well as other ancillary information. + +#. The request is decorated with various names returned from the traverser + (such as ``context``, ``view_name``, and so forth), so they can be + accessed via e.g. ``request.context`` within :term:`view` code. + +#. A :class:`~pyramid.events.ContextFound` :term:`event` is sent to any + subscribers. #. :app:`Pyramid` looks up a :term:`view` callable using the context, the request, and the view name. If a view callable doesn't exist for this @@ -86,20 +79,17 @@ processing? :class:`~pyramid.httpexceptions.HTTPNotFound` exception, which is meant to be caught by a surrounding :term:`exception view`. -#. If a view callable was found, :app:`Pyramid` attempts to call - the view function. - -#. If an :term:`authorization policy` is in use, and the view was protected - by a :term:`permission`, :app:`Pyramid` passes the context, the request, - and the view_name to a function which determines whether the view being - asked for can be executed by the requesting user, based on credential - information in the request and security information attached to the - context. If it returns ``True``, :app:`Pyramid` calls the view callable - to obtain a response. If it returns ``False``, it raises a - :class:`~pyramid.httpexceptions.HTTPForbidden` exception, which is meant - to be called by a surrounding :term:`exception view`. - -#. If any exception was raised within a :term:`root factory`, by +#. If a view callable was found, :app:`Pyramid` attempts to call it. If an + :term:`authorization policy` is in use, and the view configuration is + protected by a :term:`permission`, :app:`Pyramid` determines whether the + view callable being asked for can be executed by the requesting user based + on credential information in the request and security information attached + to the context. If the view execution is allowed, :app:`Pyramid` calls + the view callable to obtain a response. If view execution is forbidden, + :app:`Pyramid` raises a :class:`~pyramid.httpexceptions.HTTPForbidden` + exception. + +#. If any exception is raised within a :term:`root factory`, by :term:`traversal`, by a :term:`view callable` or by :app:`Pyramid` itself (such as when it raises :class:`~pyramid.httpexceptions.HTTPNotFound` or :class:`~pyramid.httpexceptions.HTTPForbidden`), the router catches the @@ -128,9 +118,8 @@ processing? .. image:: router.png -This is a very high-level overview that leaves out various details. -For more detail about subsystems invoked by the :app:`Pyramid` router -such as traversal, URL dispatch, views, and event processing, see -:ref:`urldispatch_chapter`, :ref:`views_chapter`, and -:ref:`events_chapter`. +This is a very high-level overview that leaves out various details. For more +detail about subsystems invoked by the :app:`Pyramid` router such as +traversal, URL dispatch, views, and event processing, see +:ref:`urldispatch_chapter`, :ref:`views_chapter`, and :ref:`events_chapter`. diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index f4ebef154..a7fc5d33c 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -24,11 +24,11 @@ The Startup Process ------------------- The easiest and best-documented way to start and serve a :app:`Pyramid` -application is to use the ``pserve`` command against a -:term:`PasteDeploy` ``.ini`` file. This uses the ``.ini`` file to infer -settings and starts a server listening on a port. For the purposes of this -discussion, we'll assume that you are using this command to run your -:app:`Pyramid` application. +application is to use the ``pserve`` command against a :term:`PasteDeploy` +``.ini`` file. This uses the ``.ini`` file to infer settings and starts a +server listening on a port. For the purposes of this discussion, we'll +assume that you are using this command to run your :app:`Pyramid` +application. Here's a high-level time-ordered overview of what happens when you press ``return`` after running ``pserve development.ini``. @@ -56,11 +56,12 @@ Here's a high-level time-ordered overview of what happens when you press #. The framework finds all :mod:`logging` related configuration in the ``.ini`` file and uses it to configure the Python standard library logging - system for this application. + system for this application. See :ref:`logging_config` for more + information. -#. The application's *constructor* (named by the entry point reference or +#. The application's *constructor* named by the entry point reference or dotted Python name on the ``use=`` line of the section representing your - :app:`Pyramid` application) is passed the key/value parameters mentioned + :app:`Pyramid` application is passed the key/value parameters mentioned within the section in which it's defined. The constructor is meant to return a :term:`router` instance, which is a :term:`WSGI` application. @@ -76,12 +77,14 @@ Here's a high-level time-ordered overview of what happens when you press Note that the constructor function accepts a ``global_config`` argument, which is a dictionary of key/value pairs mentioned in the ``[DEFAULT]`` - section of an ``.ini`` file. It also accepts a ``**settings`` argument, - which collects another set of arbitrary key/value pairs. The arbitrary - key/value pairs received by this function in ``**settings`` will be - composed of all the key/value pairs that are present in the ``[app:main]`` - section (except for the ``use=`` setting) when this function is called by - when you run ``pserve``. + section of an ``.ini`` file (if `[DEFAULT] + <http://docs.pylonsproject.org/projects/pyramid/dev/narr/paste.html#defaults-section-of-a-pastedeploy-ini-file>`__ + is present). It also accepts a ``**settings`` argument, which collects + another set of arbitrary key/value pairs. The arbitrary key/value pairs + received by this function in ``**settings`` will be composed of all the + key/value pairs that are present in the ``[app:main]`` section (except for + the ``use=`` setting) when this function is called by when you run + ``pserve``. Our generated ``development.ini`` file looks like so: @@ -95,7 +98,8 @@ Here's a high-level time-ordered overview of what happens when you press will receive the key/value pairs ``{'pyramid.reload_templates':'true', 'pyramid.debug_authorization':'false', 'pyramid.debug_notfound':'false', 'pyramid.debug_routematch':'false', 'pyramid.debug_templates':'true', - 'pyramid.default_locale_name':'en'}``. + 'pyramid.default_locale_name':'en'}``. See :ref:`environment_chapter` for + the meanings of these keys. #. The ``main`` function first constructs a :class:`~pyramid.config.Configurator` instance, passing a root resource @@ -103,10 +107,6 @@ Here's a high-level time-ordered overview of what happens when you press ``settings`` dictionary captured via the ``**settings`` kwarg as its ``settings`` argument. - The root resource factory is invoked on every request to retrieve the - application's root resource. It is not called during startup, only when a - request is handled. - The ``settings`` dictionary contains all the options in the ``[app:main]`` section of our .ini file except the ``use`` option (which is internal to PasteDeploy) such as ``pyramid.reload_templates``, diff --git a/docs/narr/tb_introspector.png b/docs/narr/tb_introspector.png Binary files differnew file mode 100644 index 000000000..4ae406a86 --- /dev/null +++ b/docs/narr/tb_introspector.png diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index fb9dd56c2..11318d9eb 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -403,14 +403,6 @@ The language definition documentation for Chameleon ZPT-style templates is available from `the Chameleon website <http://chameleon.repoze.org/>`_. -.. warning:: - - :term:`Chameleon` only works on :term:`CPython` platforms and - :term:`Google App Engine`. On :term:`Jython` and other non-CPython - platforms, you should use Mako (see :ref:`mako_templates`) or - ``pyramid_jinja2`` instead. See - :ref:`available_template_system_bindings`. - Given a :term:`Chameleon` ZPT template named ``foo.pt`` in a directory in your application named ``templates``, you can render the template as a :term:`renderer` like so: diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 05e851fde..7ee432fa7 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -52,7 +52,7 @@ The suggested mechanism for unit and integration testing of a :app:`Pyramid` application is the Python :mod:`unittest` module. Although this module is named :mod:`unittest`, it is actually capable of driving both unit and integration tests. A good :mod:`unittest` tutorial is available within `Dive -Into Python <http://diveintopython.org/unit_testing/index.html>`_ by Mark +Into Python <http://diveintopython.nfshost.com/unit_testing/index.html>`_ by Mark Pilgrim. :app:`Pyramid` provides a number of facilities that make unit, integration, diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 9ceb20f21..35613ea1b 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -65,7 +65,7 @@ example: config.add_view(myview, route_name='myroute') When a :term:`view callable` added to the configuration by way of -:meth:`~pyramid.config.Configurator.add_view` bcomes associated with a route +:meth:`~pyramid.config.Configurator.add_view` becomes associated with a route via its ``route_name`` predicate, that view callable will always be found and invoked when the associated route pattern matches during a request. @@ -101,7 +101,7 @@ that references ``myroute`` as a ``route_name`` parameter: def myview(request): return Response('OK') -THe above combination of ``add_route`` and ``scan`` is completely equivalent +The above combination of ``add_route`` and ``scan`` is completely equivalent to using the previous combination of ``add_route`` and ``add_view``. .. index:: diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index af5d7f242..03000629c 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -621,6 +621,7 @@ against the ``amethod`` method could be spelled equivalently as the below: def amethod(self): return Response('hello') + .. index:: single: add_view @@ -658,6 +659,186 @@ configurations, you don't need to issue a :term:`scan` in order for the view configuration to take effect. .. index:: + single: view_defaults class decorator + +.. _view_defaults: + +``@view_defaults`` Class Decorator +---------------------------------- + +.. note:: + + This feature is new in Pyramid 1.3. + +If you use a class as a view, you can use the +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. + +For instance, if you've got a class that has methods that represent "REST +actions", all which are mapped to the same route, but different request +methods, instead of this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') + + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(request_method='GET') + def get(self): + return Response('get') + + @view_config(request_method='POST') + def post(self): + return Response('post') + + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +In the above example, we were able to take the ``route_name='rest'`` argument +out of the call to each individual ``@view_config`` statement, because we +used a ``@view_defaults`` class decorator to provide the argument as a +default to each view method it possessed. + +Arguments passed to ``@view_config`` will override any default passed to +``@view_defaults``. + +The ``view_defaults`` class decorator can also provide defaults to the +:meth:`pyramid.config.Configurator.add_view` directive when a decorated class +is passed to that directive as its ``view`` argument. For example, instead +of this: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.config import Configurator + + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + if __name__ == '__main__': + config = Configurator() + config.add_route('rest', '/rest') + config.add_view( + RESTView, route_name='rest', attr='get', request_method='GET') + config.add_view( + RESTView, route_name='rest', attr='post', request_method='POST') + config.add_view( + RESTView, route_name='rest', attr='delete', request_method='DELETE') + +To reduce the amount of repetion in the ``config.add_view`` statements, we +can move the ``route_name='rest'`` argument to a ``@view_default`` class +decorator on the RESTView class: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + from pyramid.config import Configurator + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + if __name__ == '__main__': + config = Configurator() + config.add_route('rest', '/rest') + config.add_view(RESTView, attr='get', request_method='GET') + config.add_view(RESTView, attr='post', request_method='POST') + config.add_view(RESTView, attr='delete', request_method='DELETE') + +:class:`pyramid.view.view_defaults` accepts the same set of arguments that +:class:`pyramid.view.view_config` does, and they have the same meaning. Each +argument passed to ``view_defaults`` provides a default for the view +configurations of methods of the class it's decorating. + +Normal Python inheritance rules apply to defaults added via +``view_defaults``. For example: + +.. code-block:: python + :linenos: + + @view_defaults(route_name='rest') + class Foo(object): + pass + + class Bar(Foo): + pass + +The ``Bar`` class above will inherit its view defaults from the arguments +passed to the ``view_defaults`` decorator of the ``Foo`` class. To prevent +this from happening, use a ``view_defaults`` decorator without any arguments +on the subclass: + +.. code-block:: python + :linenos: + + @view_defaults(route_name='rest') + class Foo(object): + pass + + @view_defaults() + class Bar(Foo): + pass + +The ``view_defaults`` decorator only works as a class decorator; using it +against a function or a method will produce nonsensical results. + +.. index:: single: view security pair: security; view diff --git a/docs/tutorials/wiki/NOTE-relocatable.txt b/docs/tutorials/wiki/NOTE-relocatable.txt new file mode 100644 index 000000000..cec2639f3 --- /dev/null +++ b/docs/tutorials/wiki/NOTE-relocatable.txt @@ -0,0 +1,13 @@ +We specifically use relative package references where possible so this demo +works even if the user names their package (in the 'bin/paster create -t +zodb ...' step) something other than 'tutorial'. + +Specifically: + +- use relative imports +- use plain relative URLs for resources (like stylesheets and images) in + page templates. + +Direct uses of the package name, like in __init__.py 'config.scan()' +statements, are already adjusted by the paster/pcreate, so we don't have to +worry about them. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 1835ce7ea..fa18d4a41 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -4,7 +4,7 @@ Adding Authorization Our application currently allows anyone with access to the server to view, 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 +our application to allow people who 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 :term:`authorization` and :term:`authentication`. We'll make @@ -27,8 +27,8 @@ 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add Authentication and Authorization Policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We'll change our package's ``__init__.py`` file to enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable @@ -42,7 +42,7 @@ declarative security checking. We need to import the new policies: Then, we'll add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 20-25 + :lines: 17-22 :linenos: :language: python @@ -60,8 +60,8 @@ look like so: :linenos: :language: python -Adding ``security.py`` -~~~~~~~~~~~~~~~~~~~~~~ +Add ``security.py`` +~~~~~~~~~~~~~~~~~~~ Add a ``security.py`` module within your package (in the same directory as ``__init__.py``, ``views.py``, etc.) with the following @@ -73,17 +73,16 @@ content: The ``groupfinder`` function defined here is an :term:`authentication policy` "callback"; it is a callable that accepts a userid and a request. If the -userid exists in the system, the callback will -return a sequence of group identifiers (or an empty sequence if the user -isn't a member of any groups). If the userid *does not* exist in the system, -the callback will return ``None``. In a production system, user and group data will -most often come from a database, but here we use "dummy" data to represent -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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +userid exists in the system, the callback will return a sequence of group +identifiers (or an empty sequence if the user isn't a member of any groups). +If the userid *does not* exist in the system, the callback will return +``None``. In a production system, user and group data will most often come +from a database, but here we use "dummy" data to represent 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). + +Give 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 @@ -119,8 +118,8 @@ Our resulting ``models.py`` file will now look like so: :linenos: :language: python -Adding Login and Logout Views -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add Login and Logout Views +~~~~~~~~~~~~~~~~~~~~~~~~~~ We'll add a ``login`` view which renders a login form and processes the post from the login form, checking credentials. @@ -129,18 +128,24 @@ We'll also add a ``logout`` view to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -We'll add a different file (for presentation convenience) to add login -and logout views. Add a file named ``login.py`` to your application -(in the same directory as ``views.py``) with the following content: +We'll add these views to the existing ``views.py`` file we have in our +project. Here's what the ``login`` view callable will look like: + +.. literalinclude:: src/authorization/tutorial/views.py + :pyobject: login + :linenos: + :language: python + +Here's what the ``logout`` view callable will look like: -.. literalinclude:: src/authorization/tutorial/login.py +.. literalinclude:: src/authorization/tutorial/views.py + :pyobject: logout :linenos: :language: python -Note that the ``login`` view callable in the ``login.py`` file has *two* view -configuration decorators. The order of these decorators is unimportant. -Each just adds a different :term:`view configuration` for the ``login`` view -callable. +Note that the ``login`` view callable has *two* view configuration +decorators. The order of these decorators is unimportant. Each just adds a +different :term:`view configuration` for the ``login`` view callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a @@ -157,14 +162,18 @@ login form. Before being allowed to continue on to the add or edit form, he will have to provide credentials that give him permission to add or edit via this login form. -Changing Existing Views -~~~~~~~~~~~~~~~~~~~~~~~ +Note that we're relying on some additional imports within the bodies of these +views (e.g. ``remember`` and ``forget``). We'll see a rendering of the +entire views.py file a little later here to show you where those come from. -Then we need to change each of our ``view_page``, ``edit_page`` and -``add_page`` views in ``views.py`` to pass a "logged in" parameter -into its template. We'll add something like this to each view body: +Change Existing Views +~~~~~~~~~~~~~~~~~~~~~ + +In order to indicate whether the current user is logged in, we need to change +each of our ``view_page``, ``edit_page`` and ``add_page`` views in +``views.py`` to pass a "logged in" parameter into its template. We'll add +something like this to each view body: -.. ignore-next-block .. code-block:: python :linenos: @@ -175,7 +184,6 @@ We'll then change the return value of each view that has an associated ``renderer`` to pass the resulting ``logged_in`` value to the template. For example: -.. ignore-next-block .. code-block:: python :linenos: @@ -184,8 +192,8 @@ template. For example: logged_in = logged_in, edit_url = edit_url) -Adding ``permission`` Declarations to our ``view_config`` Decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add ``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` @@ -216,11 +224,11 @@ decorators. To do so, within ``views.py``: function consults the ``GROUPS`` data structure. This means that the ``editor`` user can add and edit pages. -Adding the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add the ``login.pt`` Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add a ``login.pt`` template to your templates directory. It's -referred to within the login view we just added to ``login.py``. +referred to within the login view we just added to ``views.py``. .. literalinclude:: src/authorization/tutorial/templates/login.pt :language: xml @@ -241,8 +249,8 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -Seeing Our Changes To ``views.py`` and our Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +See Our Changes To ``views.py`` and our Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Our ``views.py`` module will look something like this when we're done: @@ -262,8 +270,8 @@ Our ``view.pt`` template will look something like this when we're done: :linenos: :language: xml -Viewing the Application in a Browser -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +View the Application in a Browser +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can finally examine our application in a browser. The views we'll try are as follows: diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index 47cac597b..56f817a85 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -2,7 +2,7 @@ Basic Layout ============ -The starter files generated by the ``pyramid_zodb`` scaffold are basic, but +The starter files generated by the ``zodb`` scaffold are basic, but they provide a good orientation for the high-level patterns common to most :term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` based) projects. @@ -10,8 +10,8 @@ The source code for this tutorial stage can be browsed via `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/ <http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/>`_. -App Startup with ``__init__.py`` --------------------------------- +Appplication Configuration with ``__init__.py`` +------------------------------------------------ A directory on disk can be turned into a Python :term:`package` by containing an ``__init__.py`` file. Even if empty, this marks a directory as a Python @@ -35,7 +35,7 @@ point happens to be the ``main`` function within the file named #. *Line 12*. We construct a :term:`Configurator` with a :term:`root factory` and the settings keywords parsed by :term:`PasteDeploy`. The root - factory is named ``get_root``. + factory is named ``root_factory``. #. *Line 13*. Register a 'static view' which answers requests which start with with URL path ``/static`` using the @@ -45,16 +45,19 @@ point happens to be the ``main`` function within the file named ``http://localhost:6543/static/`` and below. The first argument is the "name" ``static``, which indicates that the URL path prefix of the view will be ``/static``. the The second argument of this tag is the "path", - which is an :term:`asset specification`, so it finds the resources it - should serve within the ``static`` directory inside the ``tutorial`` - package. + which is a relative :term:`asset specification`, so it finds the resources + it should serve within the ``static`` directory inside the ``tutorial`` + package. The scaffold could have alternately used an *absolute* asset + specification as the path (``tutorial:static``) but it does not. #. *Line 14*. Perform a :term:`scan`. A scan will find :term:`configuration - decoration`, such as view configuration decorators - (e.g. ``@view_config``) in the source code of the ``tutorial`` package and - will take actions based on these decorators. The argument to - :meth:`~pyramid.config.Configurator.scan` is the package name to scan, - which is ``tutorial``. + decoration`, such as view configuration decorators (e.g. ``@view_config``) + in the source code of the ``tutorial`` package and will take actions based + on these decorators. We don't pass any arguments to + :meth:`~pyramid.config.Configurator.scan`, which implies that the scan + should take place in the current package (in this case, ``tutorial``). + The scaffold could have equivalently said ``config.scan('tutorial')`` but + it chose to omit the package name argument. #. *Line 15*. Use the :meth:`pyramid.config.Configurator.make_wsgi_app` method @@ -69,7 +72,7 @@ hierarchically in a :term:`resource tree`. This tree is consulted by tree represents the site structure, but it *also* represents the :term:`domain model` of the application, because each resource is a node stored persistently in a :term:`ZODB` database. The ``models.py`` file is -where the ``pyramid_zodb`` scaffold put the classes that implement our +where the ``zodb`` scaffold put the classes that implement our resource objects, each of which happens also to be a domain model object. Here is the source for ``models.py``: @@ -119,7 +122,7 @@ Let's try to understand the components in this module: decoration` to perform a :term:`view configuration` registration. This view configuration registration will be activated when the application is started. It will be activated by virtue of it being found as the result - of a :term:`scan` (when Line 17 of ``__init__.py`` is run). + of a :term:`scan` (when Line 14 of ``__init__.py`` is run). The ``@view_config`` decorator accepts a number of keyword arguments. We use two keyword arguments here: ``context`` and ``renderer``. @@ -131,12 +134,15 @@ Let's try to understand the components in this module: model, this view callable will be invoked. The ``renderer`` argument names an :term:`asset specification` of - ``tutorial:templates/mytemplate.pt``. This asset specification points at - a :term:`Chameleon` template which lives in the ``mytemplate.pt`` file + ``templates/mytemplate.pt``. This asset specification points at a + :term:`Chameleon` template which lives in the ``mytemplate.pt`` file within the ``templates`` directory of the ``tutorial`` package. And indeed if you look in the ``templates`` directory of this package, you'll see a ``mytemplate.pt`` template file, which renders the default home page - of the generated project. + of the generated project. This asset specification is *relative* (to the + view.py's current package). We could have alternately an used the + absolute asset specification ``tutorial:templates/mytemplate.pt``, but + chose to use the relative version. Since this call to ``@view_config`` doesn't pass a ``name`` argument, the ``my_view`` function which it decorates represents the "default" view @@ -144,7 +150,7 @@ Let's try to understand the components in this module: #. *Lines 5-6*. We define a :term:`view callable` named ``my_view``, which we decorated in the step above. This view callable is a *function* we - write generated by the ``pyramid_zodb`` scaffold that is given a + write generated by the ``zodb`` scaffold that is given a ``request`` and which returns a dictionary. The ``mytemplate.pt`` :term:`renderer` named by the asset specification in the step above will convert this dictionary to a :term:`response` on our behalf. diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index ee9c13ab2..cdf3b6092 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -18,8 +18,8 @@ The source code for this tutorial stage can be browsed via `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/models/ <http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/models/>`_. -Deleting the Database ---------------------- +Delete the Database +------------------- In the next step, we're going to remove the ``MyModel`` Python model class from our ``models.py`` file. Since this class is referred to within @@ -30,8 +30,8 @@ directory before proceeding any further. It's always fine to do this as long as you don't care about the content of the database; the database itself will be recreated as necessary. -Making Edits to ``models.py`` ------------------------------ +Edit ``models.py`` +------------------ .. note:: @@ -73,8 +73,8 @@ front page) into the Wiki within the ``appmaker``. This will provide :term:`traversal` a :term:`resource tree` to work against when it attempts to resolve URLs to resources. -Looking at the Result of Our Edits to ``models.py`` ---------------------------------------------------- +Look at the Result of Our Edits to ``models.py`` +------------------------------------------------ The result of all of our edits to ``models.py`` will end up looking something like this: @@ -83,8 +83,8 @@ something like this: :linenos: :language: python -Viewing the Application in a Browser ------------------------------------- +View the Application in a Browser +--------------------------------- We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index c21367559..371cae8eb 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -293,12 +293,10 @@ replicate within the body of this guide, however it is available `online <http://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki/src/views/tutorial/static/pylons.css>`_. This CSS file will be accessed via -e.g. ``http://localhost:6543/static/pylons.css`` by virtue of the call to +e.g. ``/static/pylons.css`` by virtue of the call to ``add_static_view`` directive we've made in the ``__init__.py`` file. Any number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url`` e.g. ``request.static_url('{{package}}:static/foo.css')`` -within templates. +subdirectories) and are just referred to by URL. Viewing the Application in a Browser ==================================== diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index c55c310ef..330b17c86 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -127,8 +127,8 @@ Preparation, Windows .. _making_a_project: -Making a Project -================ +Make a Project +============== Your next step is to create a project. :app:`Pyramid` supplies a variety of scaffolds to generate sample projects. For this tutorial, we will use the @@ -149,14 +149,18 @@ On Windows: c:\pyramidtut> Scripts\pcreate -s zodb tutorial +.. note:: You don't have to call it `tutorial` -- the code uses + relative paths for imports and finding templates and static + resources. + .. note:: If you are using Windows, the ``zodb`` scaffold doesn't currently deal gracefully with installation into a location that contains spaces in the path. If you experience startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. -Installing the Project in "Development Mode" -============================================ +Install 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 @@ -180,8 +184,8 @@ On Windows: .. _running_tests: -Running the Tests -================= +Run the Tests +============= After you've installed the project in development mode, you may run the tests for the project. @@ -198,48 +202,53 @@ On Windows: c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q -Starting the Application -======================== +Expose Test Coverage Information +================================ -Start the application. +You can run the ``nosetests`` command to see test coverage +information. This runs the tests in the same way that ``setup.py +test`` does but provides additional "coverage" information, exposing +which lines of your project are "covered" (or not covered) by the +tests. On UNIX: .. code-block:: text - $ ../bin/pserve development.ini --reload + $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage On Windows: .. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\pserve development.ini --reload + c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ + --cover-erase --with-coverage -Exposing Test Coverage Information -================================== +Looks like the code in the ``zodb`` scaffold for ZODB projects is +missing some test coverage, particularly in the file named +``models.py``. -You can run the ``nosetests`` command to see test coverage -information. This runs the tests in the same way that ``setup.py -test`` does but provides additional "coverage" information, exposing -which lines of your project are "covered" (or not covered) by the -tests. +Start the Application +===================== + +Start the application. On UNIX: .. code-block:: text - $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage + $ ../bin/pserve development.ini --reload On Windows: .. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ - --cover-erase --with-coverage + c:\pyramidtut\tutorial> ..\Scripts\pserve development.ini --reload -Looks like the code in the ``pyramid_zodb`` scaffold for ZODB projects is -missing some test coverage, particularly in the file named -``models.py``. +.. note:: + + Your OS firewall, if any, may pop up a dialog asking for authorization + to allow python to accept incoming network connections. Visit the Application in a Browser ================================== @@ -252,10 +261,10 @@ page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. -Decisions the ``pyramid_zodb`` Scaffold Has Made For You -======================================================== +Decisions the ``zodb`` Scaffold Has Made For You +================================================ -Creating a project using the ``pyramid_zodb`` scaffold makes the following +Creating a project using the ``zodb`` scaffold makes the following assumptions: - you are willing to use :term:`ZODB` as persistent storage diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py index 2d6eb5ecb..20ee685ee 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -4,8 +4,8 @@ from pyramid_zodbconn import get_connection from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from tutorial.models import appmaker -from tutorial.security import groupfinder +from .models import appmaker +from .security import groupfinder def root_factory(request): conn = get_connection(request) @@ -20,6 +20,6 @@ def main(global_config, **settings): config = Configurator(root_factory=root_factory, settings=settings, authentication_policy=authn_policy, authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - config.scan('tutorial') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py deleted file mode 100644 index d608a7d0b..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyramid.httpexceptions import HTTPFound - -from pyramid.security import remember -from pyramid.security import forget -from pyramid.view import view_config - -from tutorial.security import USERS - -@view_config(context='tutorial.models.Wiki', name='login', - renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') -def login(request): - login_url = request.resource_url(request.context, 'login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(context='tutorial.models.Wiki', name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.resource_url(request.context), - headers = headers) - diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py index cfd13071e..d88c9c71f 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py @@ -5,4 +5,3 @@ GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, []) - diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt index f9da6c414..0d0738f7f 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt index 64e592ea9..2c7235761 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt index 14b88d16a..3597c679b 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt index d207a0c23..9dd6540cf 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/tests.py b/docs/tutorials/wiki/src/authorization/tutorial/tests.py index a4a4e2754..77e7cce29 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/tests.py @@ -5,7 +5,7 @@ from pyramid import testing class PageModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Page + from .models import Page return Page def _makeOne(self, data=u'some data'): @@ -14,11 +14,11 @@ class PageModelTests(unittest.TestCase): def test_constructor(self): instance = self._makeOne() self.assertEqual(instance.data, u'some data') - + class WikiModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Wiki + from .models import Wiki return Wiki def _makeOne(self): @@ -31,7 +31,7 @@ class WikiModelTests(unittest.TestCase): class AppmakerTests(unittest.TestCase): def _callFUT(self, zodb_root): - from tutorial.models import appmaker + from .models import appmaker return appmaker(zodb_root) def test_it(self): @@ -42,7 +42,7 @@ class AppmakerTests(unittest.TestCase): class ViewWikiTests(unittest.TestCase): def test_it(self): - from tutorial.views import view_wiki + from .views import view_wiki context = testing.DummyResource() request = testing.DummyRequest() response = view_wiki(context, request) @@ -50,7 +50,7 @@ class ViewWikiTests(unittest.TestCase): class ViewPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import view_page + from .views import view_page return view_page(context, request) def test_it(self): @@ -72,11 +72,11 @@ class ViewPageTests(unittest.TestCase): '</p>\n</div>\n') self.assertEqual(info['edit_url'], 'http://example.com/thepage/edit_page') - - + + class AddPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import add_page + from .views import add_page return add_page(context, request) def test_it_notsubmitted(self): @@ -88,7 +88,7 @@ class AddPageTests(unittest.TestCase): self.assertEqual(info['save_url'], request.resource_url( context, 'add_page', 'AnotherPage')) - + def test_it_submitted(self): context = testing.DummyResource() request = testing.DummyRequest({'form.submitted':True, @@ -102,7 +102,7 @@ class AddPageTests(unittest.TestCase): class EditPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import edit_page + from .views import edit_page return edit_page(context, request) def test_it_notsubmitted(self): @@ -112,7 +112,7 @@ class EditPageTests(unittest.TestCase): self.assertEqual(info['page'], context) self.assertEqual(info['save_url'], request.resource_url(context, 'edit_page')) - + def test_it_submitted(self): context = testing.DummyResource() request = testing.DummyRequest({'form.submitted':True, diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index a570410ca..2f0502c17 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -2,19 +2,26 @@ from docutils.core import publish_parts import re from pyramid.httpexceptions import HTTPFound + from pyramid.view import view_config -from pyramid.security import authenticated_userid -from tutorial.models import Page +from pyramid.security import ( + authenticated_userid, + remember, + forget, + ) + +from .security import USERS +from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='tutorial.models.Wiki', permission='view') +@view_config(context='.models.Wiki', permission='view') def view_wiki(context, request): return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='tutorial.models.Page', +@view_config(context='.models.Page', renderer='templates/view.pt', permission='view') def view_page(context, request): wiki = context.__parent__ @@ -38,7 +45,7 @@ def view_page(context, request): return dict(page = context, content = content, edit_url = edit_url, logged_in = logged_in) -@view_config(name='add_page', context='tutorial.models.Wiki', +@view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', permission='edit') def add_page(context, request): @@ -59,7 +66,7 @@ def add_page(context, request): return dict(page = page, save_url = save_url, logged_in = logged_in) -@view_config(name='edit_page', context='tutorial.models.Page', +@view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', permission='edit') def edit_page(context, request): @@ -72,4 +79,39 @@ def edit_page(context, request): return dict(page = context, save_url = request.resource_url(context, 'edit_page'), logged_in = logged_in) - + +@view_config(context='.models.Wiki', name='login', + renderer='templates/login.pt') +@view_config(context='pyramid.httpexceptions.HTTPForbidden', + renderer='templates/login.pt') +def login(request): + login_url = request.resource_url(request.context, 'login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(context='.models.Wiki', name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.resource_url(request.context), + headers = headers) diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py index e49a61129..b63933fc5 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py @@ -1,6 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from tutorial.models import appmaker +from .models import appmaker def root_factory(request): conn = get_connection(request) @@ -10,6 +10,6 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ config = Configurator(root_factory=root_factory, settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - config.scan('tutorial') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt index f9f351c97..557e071ed 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py index 1f3c3bb4d..8d2374be1 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py @@ -10,7 +10,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py index 157b9ac8f..4265b6bf7 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py @@ -1,7 +1,6 @@ from pyramid.view import view_config -from tutorial.models import MyModel +from .models import MyModel -@view_config(context=MyModel, - renderer='tutorial:templates/mytemplate.pt') +@view_config(context=MyModel, renderer='templates/mytemplate.pt') def my_view(request): return {'project':'tutorial'} diff --git a/docs/tutorials/wiki/src/models/tutorial/__init__.py b/docs/tutorials/wiki/src/models/tutorial/__init__.py index 2d637b9de..c59f36e7b 100644 --- a/docs/tutorials/wiki/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/models/tutorial/__init__.py @@ -1,6 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from tutorial.models import appmaker +from .models import appmaker def root_factory(request): conn = get_connection(request) @@ -10,7 +10,7 @@ def main(global_config, **settings): """ This function returns a WSGI application. """ config = Configurator(root_factory=root_factory, settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - config.scan('tutorial') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt index 14b88d16a..3597c679b 100644 --- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> diff --git a/docs/tutorials/wiki/src/models/tutorial/tests.py b/docs/tutorials/wiki/src/models/tutorial/tests.py index 51c97a95d..9fd13a18d 100644 --- a/docs/tutorials/wiki/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki/src/models/tutorial/tests.py @@ -5,7 +5,7 @@ from pyramid import testing class PageModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Page + from .models import Page return Page def _makeOne(self, data=u'some data'): @@ -14,11 +14,11 @@ class PageModelTests(unittest.TestCase): def test_constructor(self): instance = self._makeOne() self.assertEqual(instance.data, u'some data') - + class WikiModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Wiki + from .models import Wiki return Wiki def _makeOne(self): @@ -32,7 +32,7 @@ class WikiModelTests(unittest.TestCase): class AppmakerTests(unittest.TestCase): def _callFUT(self, zodb_root): - from tutorial.models import appmaker + from .models import appmaker return appmaker(zodb_root) def test_no_app_root(self): @@ -55,7 +55,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/models/tutorial/views.py b/docs/tutorials/wiki/src/models/tutorial/views.py index 2346602c9..7c1f1d228 100644 --- a/docs/tutorials/wiki/src/models/tutorial/views.py +++ b/docs/tutorials/wiki/src/models/tutorial/views.py @@ -1,5 +1,5 @@ from pyramid.view import view_config -@view_config(renderer='tutorial:templates/mytemplate.pt') +@view_config(renderer='templates/mytemplate.pt') def my_view(request): return {'project':'tutorial'} diff --git a/docs/tutorials/wiki/src/tests/tutorial/__init__.py b/docs/tutorials/wiki/src/tests/tutorial/__init__.py index 2d6eb5ecb..20ee685ee 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py @@ -4,8 +4,8 @@ from pyramid_zodbconn import get_connection from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from tutorial.models import appmaker -from tutorial.security import groupfinder +from .models import appmaker +from .security import groupfinder def root_factory(request): conn = get_connection(request) @@ -20,6 +20,6 @@ def main(global_config, **settings): config = Configurator(root_factory=root_factory, settings=settings, authentication_policy=authn_policy, authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - config.scan('tutorial') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/tests/tutorial/login.py b/docs/tutorials/wiki/src/tests/tutorial/login.py deleted file mode 100644 index d608a7d0b..000000000 --- a/docs/tutorials/wiki/src/tests/tutorial/login.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyramid.httpexceptions import HTTPFound - -from pyramid.security import remember -from pyramid.security import forget -from pyramid.view import view_config - -from tutorial.security import USERS - -@view_config(context='tutorial.models.Wiki', name='login', - renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') -def login(request): - login_url = request.resource_url(request.context, 'login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(context='tutorial.models.Wiki', name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.resource_url(request.context), - headers = headers) - diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py index cfd13071e..d88c9c71f 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/security.py +++ b/docs/tutorials/wiki/src/tests/tutorial/security.py @@ -5,4 +5,3 @@ GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, []) - diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt index f9da6c414..0d0738f7f 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt index 64e592ea9..2c7235761 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt index 14b88d16a..3597c679b 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt index d207a0c23..9dd6540cf 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index b1d4e68c3..81f7a1882 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -5,7 +5,7 @@ from pyramid import testing class PageModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Page + from .models import Page return Page def _makeOne(self, data=u'some data'): @@ -18,7 +18,7 @@ class PageModelTests(unittest.TestCase): class WikiModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Wiki + from .models import Wiki return Wiki def _makeOne(self): @@ -31,7 +31,7 @@ class WikiModelTests(unittest.TestCase): class AppmakerTests(unittest.TestCase): def _callFUT(self, zodb_root): - from tutorial.models import appmaker + from .models import appmaker return appmaker(zodb_root) def test_it(self): @@ -42,7 +42,7 @@ class AppmakerTests(unittest.TestCase): class ViewWikiTests(unittest.TestCase): def test_it(self): - from tutorial.views import view_wiki + from .views import view_wiki context = testing.DummyResource() request = testing.DummyRequest() response = view_wiki(context, request) @@ -50,7 +50,7 @@ class ViewWikiTests(unittest.TestCase): class ViewPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import view_page + from .views import view_page return view_page(context, request) def test_it(self): @@ -76,7 +76,7 @@ class ViewPageTests(unittest.TestCase): class AddPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import add_page + from .views import add_page return add_page(context, request) def test_it_notsubmitted(self): @@ -102,7 +102,7 @@ class AddPageTests(unittest.TestCase): class EditPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import edit_page + from .views import edit_page return edit_page(context, request) def test_it_notsubmitted(self): @@ -133,7 +133,7 @@ class FunctionalTests(unittest.TestCase): def setUp(self): import tempfile import os.path - from tutorial import main + from . import main self.tmpdir = tempfile.mkdtemp() dbpath = os.path.join( self.tmpdir, 'test.db') diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index a570410ca..2f0502c17 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -2,19 +2,26 @@ from docutils.core import publish_parts import re from pyramid.httpexceptions import HTTPFound + from pyramid.view import view_config -from pyramid.security import authenticated_userid -from tutorial.models import Page +from pyramid.security import ( + authenticated_userid, + remember, + forget, + ) + +from .security import USERS +from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='tutorial.models.Wiki', permission='view') +@view_config(context='.models.Wiki', permission='view') def view_wiki(context, request): return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='tutorial.models.Page', +@view_config(context='.models.Page', renderer='templates/view.pt', permission='view') def view_page(context, request): wiki = context.__parent__ @@ -38,7 +45,7 @@ def view_page(context, request): return dict(page = context, content = content, edit_url = edit_url, logged_in = logged_in) -@view_config(name='add_page', context='tutorial.models.Wiki', +@view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', permission='edit') def add_page(context, request): @@ -59,7 +66,7 @@ def add_page(context, request): return dict(page = page, save_url = save_url, logged_in = logged_in) -@view_config(name='edit_page', context='tutorial.models.Page', +@view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', permission='edit') def edit_page(context, request): @@ -72,4 +79,39 @@ def edit_page(context, request): return dict(page = context, save_url = request.resource_url(context, 'edit_page'), logged_in = logged_in) - + +@view_config(context='.models.Wiki', name='login', + renderer='templates/login.pt') +@view_config(context='pyramid.httpexceptions.HTTPForbidden', + renderer='templates/login.pt') +def login(request): + login_url = request.resource_url(request.context, 'login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(context='.models.Wiki', name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.resource_url(request.context), + headers = headers) diff --git a/docs/tutorials/wiki/src/views/tutorial/__init__.py b/docs/tutorials/wiki/src/views/tutorial/__init__.py index 009013b3f..957a0b705 100644 --- a/docs/tutorials/wiki/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/views/tutorial/__init__.py @@ -10,6 +10,6 @@ def main(global_config, **settings): """ This function returns a WSGI application. """ config = Configurator(root_factory=root_factory, settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) + config.add_static_view('static', 'static', cache_max_age=3600) config.scan('tutorial') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt index 6dbb0edde..24ed2e592 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt index 14b88d16a..3597c679b 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt index 537ae3a15..424c4302a 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt @@ -9,13 +9,13 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> + href="/static/favicon.ico" /> <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" + href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" + href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> @@ -25,7 +25,7 @@ <div class="top-small align-center"> <div> <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + src="/static/pyramid-small.png" /> </div> </div> </div> diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index 245cda682..016f5b6bb 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -4,17 +4,16 @@ import re from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config -from tutorial.models import Page +from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='tutorial.models.Wiki') +@view_config(context='.models.Wiki') def view_wiki(context, request): return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='tutorial.models.Page', - renderer='tutorial:templates/view.pt') +@view_config(context='.models.Page', renderer='templates/view.pt') def view_page(context, request): wiki = context.__parent__ @@ -33,8 +32,8 @@ def view_page(context, request): edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url) -@view_config(name='add_page', context='tutorial.models.Wiki', - renderer='tutorial:templates/edit.pt') +@view_config(name='add_page', context='.models.Wiki', + renderer='templates/edit.pt') def add_page(context, request): name = request.subpath[0] if 'form.submitted' in request.params: @@ -50,8 +49,8 @@ def add_page(context, request): page.__parent__ = context return dict(page = page, save_url = save_url) -@view_config(name='edit_page', context='tutorial.models.Page', - renderer='tutorial:templates/edit.pt') +@view_config(name='edit_page', context='.models.Page', + renderer='templates/edit.pt') def edit_page(context, request): if 'form.submitted' in request.params: context.data = request.params['body'] @@ -59,5 +58,3 @@ def edit_page(context, request): return dict(page = context, save_url = request.resource_url(context, 'edit_page')) - - diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst index 841baa8d1..1ddb8f408 100644 --- a/docs/tutorials/wiki/tests.rst +++ b/docs/tutorials/wiki/tests.rst @@ -6,22 +6,21 @@ We will now add tests for the models and the views and a few functional tests in the ``tests.py``. Tests ensure that an application works, and that it continues to work after some changes are made in the future. -Testing the Models -================== +Test the Models +=============== -We write tests for the model -classes and the appmaker. Changing ``tests.py``, we'll write a separate test -class for each model class, and we'll write a test class for the -``appmaker``. +We write tests for the model classes and the appmaker. Changing +``tests.py``, we'll write a separate test class for each model class, and +we'll write a test class for the ``appmaker``. To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_zodb`` project generator. We'll add three test +result of the ``zodb`` project generator. We'll add three test classes: one for the ``Page`` model named ``PageModelTests``, one for the ``Wiki`` model named ``WikiModelTests``, and one for the appmaker named ``AppmakerTests``. -Testing the Views -================= +Test the Views +============== We'll modify our ``tests.py`` file, adding tests for each view function we added above. As a result, we'll *delete* the ``ViewTests`` test in the file, @@ -38,8 +37,8 @@ tested in the unit tests, like logging in, logging out, checking that the ``viewer`` user cannot add or edit pages, but the ``editor`` user can, and so on. -Viewing the results of all our edits to ``tests.py`` -==================================================== +View the results of all our edits to ``tests.py`` +================================================= Once we're done with the ``tests.py`` module, it will look a lot like the below: @@ -48,8 +47,8 @@ below: :linenos: :language: python -Running the Tests -================= +Run the Tests +============= We can run these tests by using ``setup.py test`` in the same way we did in :ref:`running_tests`. However, first we must edit our ``setup.py`` to diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index df5e228fd..ab04ea405 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -4,27 +4,22 @@ Adding Authorization ==================== -Our application currently allows anyone with access to the server to -view, edit, and add pages to our wiki. For purposes of demonstration -we'll change our application to allow only people whom possess a -specific username (`editor`) 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 :term:`authorization` and -:term:`authentication`. We'll make use of both features to provide security -to our application. - -We will add an :term:`authentication policy` and an -:term:`authorization policy` to our :term:`application -registry`, add a ``security.py`` module, create a :term:`root factory` -with an :term:`ACL`, and add :term:`permission` declarations to -the ``edit_page`` and ``add_page`` views. - -Then we will add ``login`` and ``logout`` views, and modify the -existing views to make them return a ``logged_in`` flag to the -renderer. - -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. +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. We'll make use of both features to provide security +to our application. Our application currently allows anyone with access to +the server to view, edit, and add pages to our wiki. We'll change our +application to allow only people whom possess a specific username (`editor`) +to add and edit wiki pages but we'll continue allowing anyone with access to +the server to view pages. + +To do so, we'll add an :term:`authentication policy` and an +:term:`authorization policy`. We'll also add a ``security.py`` module, +create a :term:`root factory` with an :term:`ACL`, and add :term:`permission` +declarations to the ``edit_page`` and ``add_page`` views. Then we'll add +``login`` and ``logout`` views, and modify the existing views to make them +return a ``logged_in`` flag to the renderer. 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. The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/ @@ -54,7 +49,7 @@ inside our ``models.py`` file. Add the following statements to your ``models.py`` file: .. literalinclude:: src/authorization/tutorial/models.py - :lines: 3-4,45-50 + :lines: 1-4,35-39 :linenos: :language: python @@ -92,14 +87,14 @@ We'll change our ``__init__.py`` file to enable an declarative security checking. We need to import the new policies: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 2-3,8 + :lines: 2-3,7 :linenos: :language: python Then, we'll add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 15-21 + :lines: 16-22 :linenos: :language: python @@ -111,49 +106,12 @@ represented by this policy: it is required. The ``callback`` is a ``groupfinder`` function in the current directory's ``security.py`` file. We haven't added that module yet, but we're about to. -We'll also change ``__init__.py``, adding a call to -:meth:`pyramid.config.Configurator.add_view` that points at our ``login`` -:term:`view callable`. This is also known as a :term:`forbidden view`: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 25,41-43 - :linenos: - :language: python - -A forbidden view configures our newly created login view to show up when -:app:`Pyramid` detects that a view invocation can not be authorized. - -A ``logout`` :term:`view callable` will allow users to log out later: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 26,34 - :linenos: - :language: python - -We'll also add ``permission`` arguments with the value ``edit`` to the -``edit_page`` and ``add_page`` views. This indicates that the view -callables which these views reference cannot be invoked without the -authenticated user possessing the ``edit`` permission with respect to the -current context. - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 37-40 - :linenos: - :language: python - -Adding these ``permission`` arguments causes Pyramid to make the -assertion that only users who possess the effective ``edit`` permission at -the time of the request may invoke those two views. 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 views associated with the ``add_page`` or -``edit_page`` routes. - Viewing Your Changes ~~~~~~~~~~~~~~~~~~~~ -When we're done configuring a root factory, adding an authorization policy, -and adding views, your application's ``__init__.py`` will look like this: +When we're done configuring a root factory, adding a authentication and +authorization policies, and adding routes for ``/login`` and ``/logout``, +your application's ``__init__.py`` will look like this: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: @@ -191,30 +149,54 @@ views, the ``editor`` user should be able to add and edit pages. Adding Login and Logout Views ----------------------------- -We'll add a ``login`` view callable which renders a login form and -processes the post from the login form, checking credentials. +To our ``views.py`` we'll add a ``login`` view callable which renders a login +form and processes the post from the login form, checking credentials. We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -We'll add a different file (for presentation convenience) to add login -and the logout view callables. Add a file named ``login.py`` to your -application (in the same directory as ``views.py``) with the following -content: +The ``login`` view callable will look something like this: -.. literalinclude:: src/authorization/tutorial/login.py +.. literalinclude:: src/authorization/tutorial/views.py + :pyobject: login :linenos: :language: python +The ``logout`` view callable will look something like this: + +.. literalinclude:: src/authorization/tutorial/views.py + :pyobject: logout + :linenos: + :language: python + +The ``login`` view callable is decorated with two ``@view_config`` +decorators, one which associates it with the ``login`` route, the other which +associates it with the ``HTTPForbidden`` context. The one which associates +it with the ``login`` route makes it visible when we visit ``/login``. The +one which associates it with the ``HTTPForbidden`` context makes it the +:term:`forbidden view`. The forbidden view is displayed whenever Pyramid or +your application raises an HTTPForbidden exception. In this case, we'll be +relying on the forbidden view to show the login form whenver someone attempts +to execute an action which they're not yet authorized to perform. + +The ``logout`` view callable is decorated with a ``@view_config`` decorator +which associates it with the ``logout`` route. This makes it visible when we +visit ``/login``. + +We'll need to import some stuff to service the needs of these two functions: +the ``HTTPForbidden`` exception, a number of values from the +``pyramid.security`` module, and a value from our newly added +``tutorial.security`` package. + Changing Existing Views ----------------------- Then we need to change each of our ``view_page``, ``edit_page`` and -``add_page`` views in ``views.py`` to pass a "logged in" parameter to its -template. We'll add something like this to each view body: +``add_page`` view callables in ``views.py``. Within each of these views, +we'll need to pass a "logged in" parameter to its template. We'll add +something like this to each view body: -.. ignore-next-block .. code-block:: python :linenos: @@ -224,7 +206,6 @@ template. We'll add something like this to each view body: We'll then change the return value of these views to pass the `resulting `logged_in`` value to the template, e.g.: -.. ignore-next-block .. code-block:: python :linenos: @@ -233,6 +214,28 @@ We'll then change the return value of these views to pass the `resulting logged_in = logged_in, edit_url = edit_url) +We'll also need to add a ``permission`` value to the ``@view_config`` +decorator for each of the ``add_page`` and ``edit_page`` view callables. For +each, we'll add ``permission='edit'``, for example: + +.. code-block:: python + :linenos: + + @view_config(route_name='edit_page', renderer='templates/edit.pt', + permission='edit') + +See the ``permission='edit'`` we added there? This indicates that the view +callables which these views reference cannot be invoked without the +authenticated user possessing the ``edit`` permission with respect to the +current :term:`context`. + +Adding these ``permission`` arguments causes Pyramid to make the assertion +that only users who possess the effective ``edit`` permission at the time of +the request may invoke those two views. 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 views associated with the ``add_page`` or ``edit_page`` routes. + Adding the ``login.pt`` Template -------------------------------- diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 8dc886373..77658970d 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -2,82 +2,96 @@ Basic Layout ============ -The starter files generated by the ``pyramid_routesalchemy`` scaffold are -basic, but they provide a good orientation for the high-level patterns common -to most :term:`url dispatch` -based :app:`Pyramid` projects. +The starter files generated by the ``alchemy`` scaffold are very basic, but +they provide a good orientation for the high-level patterns common to most +:term:`url dispatch` -based :app:`Pyramid` projects. The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/basiclayout/ <http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/basiclayout/>`_. -App Startup with ``__init__.py`` --------------------------------- +Application Configuration with ``__init__.py`` +---------------------------------------------- A directory on disk can be turned into a Python :term:`package` by containing an ``__init__.py`` file. Even if empty, this marks a directory as a Python -package. We use ``__init__.py`` both as a package marker and to contain -configuration code. +package. We use ``__init__.py`` both as a marker indicating the directory +it's contained within is a package, and to contain configuration code. Our +``__init__.py`` file will look like this: -The generated ``development.ini`` file is read by ``pserve`` which looks for -the application module in the ``use`` variable of the ``app:main`` -section. The *entry point* is defined in the Setuptools configuration of this -module, specifically in the ``setup.py`` file. For this tutorial, the *entry -point* is defined as ``tutorial:main`` and points to a function named -``main``. + .. literalinclude:: src/basiclayout/tutorial/__init__.py + :linenos: + :language: py -First we need some imports to support later code: +Let's go over this piece-by-piece. First, we need some imports to support +later code: .. literalinclude:: src/basiclayout/tutorial/__init__.py :end-before: main :linenos: :language: py -Next we define the main function and create a SQLAlchemy database engine from -the ``sqlalchemy.`` prefixed settings in the ``development.ini`` file's -``[app:main]`` section. This will be a URI (something like -``sqlite://``): +``__init__.py`` defines a function named ``main``. Here is the entirety of +the ``main`` function we've defined in our ``__init__.py``: + + .. literalinclude:: src/basiclayout/tutorial/__init__.py + :pyobject: main + :linenos: + :language: py + +When you invoke the ``pserve development.ini`` command, the ``main`` function +above is executed. It accepts some settings and returns a :term:`WSGI` +application. You can read :ref:`startup_chapter` for details about *how* +this function is found and called when you run ``pserve``, but for purposes +of brevity, we'll elide the details here. + +The main function first creates a 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://``): .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 6-9 + :lines: 9 :linenos: :language: py -We then initialize our SQL database using SQLAlchemy, passing -it the engine: +``main`` then initializes our SQL database using SQLAlchemy, passing it the +engine: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 :language: py -The next step is to construct a :term:`Configurator`: +The next step of ``main`` is to construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 :language: py ``settings`` is passed to the Configurator as a keyword argument with the -dictionary values passed by PasteDeploy as the ``**settings`` argument. This -will be a dictionary of settings parsed from the ``.ini`` file, which -contains deployment-related values such as ``pyramid.reload_templates``, +dictionary values passed as the ``**settings`` argument. This will be a +dictionary of settings parsed from the ``.ini`` file, which contains +deployment-related values such as ``pyramid.reload_templates``, ``db_string``, etc. -We now can call :meth:`pyramid.config.Configurator.add_static_view` with the -arguments ``static`` (the name), and ``tutorial:static`` (the path): +``'main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with +two arguments: ``static`` (the name), and ``static`` (the path): .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 12 :language: py -This registers a static resource view which will match any URL that starts with -``/static/``. This will serve up static resources for us from within the -``static`` directory of our ``tutorial`` package, in this case, -via ``http://localhost:6543/static/`` and below. With this declaration, -we're saying that any URL that starts with ``/static`` should go to the -static view; any remainder of its path (e.g. the ``/foo`` in -``/static/foo``) will be used to compose a path to a static file resource, -such as a CSS file. +This registers a static resource view which will match any URL that starts +with the prefix ``/static`` (by virtue of the first argument to add_static +view). This will serve up static resources for us from within the ``static`` +directory of our ``tutorial`` package, in this case, via +``http://localhost:6543/static/`` and below (by virtue of the second argument +to add_static_view). With this declaration, we're saying that any URL that +starts with ``/static`` should go to the static view; any remainder of its +path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to +a static file resource, such as a CSS file. -Using the configurator we can also register a :term:`route configuration` +Using the configurator ``main`` also registers a :term:`route configuration` via the :meth:`pyramid.config.Configurator.add_route` method that will be used when the URL is ``/``: @@ -88,44 +102,77 @@ used when the URL is ``/``: Since this route has a ``pattern`` equalling ``/`` it is the route that will be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. -Mapping the ``home`` route to code is done by registering a view. You will -use :meth:`pyramid.config.Configurator.add_view` in :term:`URL dispatch` to -register views for the routes, mapping your patterns to code: +``main`` next calls the ``scan`` method of the configurator, which will +recursively scan our ``tutorial`` package, looking for ``@view_config`` (and +other special) decorators. When it finds a ``@view_config`` decorator, a +view configuration will be registered, which will allow one of our +application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14-15 + :lines: 14 :language: py -The first positional ``add_view`` argument ``tutorial.views.my_view`` is the -dotted name to a *function* we write (generated by the -``pyramid_routesalchemy`` scaffold) that is given a ``request`` object and -which returns a response or a dictionary. This view also names a -``renderer``, which is a template which lives in the ``templates`` -subdirectory of the package. When the ``tutorial.views.my_view`` view -returns a dictionary, a :term:`renderer` will use this template to create a -response. - -Finally, we use the :meth:`pyramid.config.Configurator.make_wsgi_app` -method to return a :term:`WSGI` application: +Finally, ``main`` is finished configuring things, so it uses the +:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a +:term:`WSGI` application: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 16 + :lines: 15 :language: py -Our final ``__init__.py`` file will look like this: +View Declarations via ``views.py`` +---------------------------------- - .. literalinclude:: src/basiclayout/tutorial/__init__.py +Mapping a :term:`route` to code that will be executed when that route's +pattern matches is done by registering a :term:`view configuration`. Our +application uses the :meth:`pyramid.view.view_config` decorator to map view +callables to each route, thereby mapping URL patterns to code. + +Here is the entirety of code in the ``views.py`` file within our package: + + .. literalinclude:: src/basiclayout/tutorial/views.py :linenos: :language: py +The important part to point out here is the ``@view_config`` decorator which +sits atop the ``my_view`` function. In fact, ``@view_config`` is so +important that we're going to ignore the rest of the code in the module at +this point just to explain it. The ``@view_config`` decorator associates the +function it decorates with a :term:`view configuration`. The view +configuration names a ``route_name`` (``home``), and names a ``renderer``, +which is a template which lives in the ``templates`` subdirectory of the +package. + +As the result of this view configuration, when the pattern associated with +the view named ``home`` is matched during a request, the function named +``my_view`` will be executed. The the function named ``my_view`` returns a +dictionary; the renderer will use the ``templates/mytemplate.pt`` template to +create a response based on the values in the dictionary. + +Note that the decorated function named ``my_view`` accepts a single argument +named ``request``. This is the standard call signature for a Pyramid +:term:`view callable`. + +Remember in our ``__init__.py`` when we executed the +:meth:`pyramid.config.Configurator.scan` method, e.g. ``config.scan()``? The +purpose of calling the scan method was to find and process this +``@view_config`` decorator in order to create a view configuration within our +application. Without being processed by ``scan``, the decorator effectively +does nothing. ``@view_config`` is inert without being detected via a +:term:`scan`. + Content Models with ``models.py`` --------------------------------- -In a SQLAlchemy-based application, a *model* object is an object -composed by querying the SQL database which backs an application. -SQLAlchemy is an "object relational mapper" (an ORM). The -``models.py`` file is where the ``pyramid_routesalchemy`` scaffold -put the classes that implement our models. +In a SQLAlchemy-based application, a *model* object is an object composed by +querying the SQL database. The ``models.py`` file is where the ``alchemy`` +scaffold put the classes that implement our models. + +Here is the complete source for ``models.py``: + + .. literalinclude:: src/basiclayout/tutorial/models.py + :linenos: + :language: py Let's take a look. First, we need some imports to support later code. @@ -137,7 +184,7 @@ Let's take a look. First, we need some imports to support later code. Next we set up a SQLAlchemy "DBSession" object: .. literalinclude:: src/basiclayout/tutorial/models.py - :lines: 15-16 + :lines: 16 :linenos: :language: py @@ -161,30 +208,6 @@ within the ``__init__`` function itself. The ``MyModel`` class also has a ``__tablename__`` attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class. -Next we define a function named ``populate`` which adds a single -model instance into our SQL storage and commits a transaction: - - .. literalinclude:: src/basiclayout/tutorial/models.py - :pyobject: populate - :linenos: - :language: py - -The function doesn't do a lot in this case, but it's there to illustrate -how an application requiring many objects to be set up could work. - -Lastly we have a function named ``initialize_sql`` which receives a SQL -database engine and binds it to our SQLAlchemy DBSession object. It also -calls the ``populate`` function, to do initial database population. This -is the initialization function that is called from __init__.py above. - - .. literalinclude:: src/basiclayout/tutorial/models.py - :pyobject: initialize_sql - :linenos: - :language: py - -Here is the complete source for ``models.py``: - - .. literalinclude:: src/basiclayout/tutorial/models.py - :linenos: - :language: py +That's about all there is to it to models, views, and initialization code in +our stock application. diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 083ec0aa8..cd295e993 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -15,29 +15,25 @@ Making Edits to ``models.py`` .. note:: - There is nothing automagically special about the filename - ``models.py``. A project may have many models throughout its - codebase in arbitrarily-named files. Files implementing models - often have ``model`` in their filenames (or they may live in a - Python subpackage of your application package named ``models``) , - but this is only by convention. + There is nothing automagically special about the filename ``models.py``. A + project may have many models throughout its codebase in arbitrarily-named + files. Files implementing models often have ``model`` in their filenames + (or they may live in a Python subpackage of your application package named + ``models``) , but this is only by convention. -The first thing we want to do is remove the stock ``MyModel`` class from the -generated ``models.py`` file. The ``MyModel`` class is only a sample and -we're not going to use it. - -Next, we'll remove the :class:`sqlalchemy.Unicode` import and replace it -with :class:`sqlalchemy.Text`. +Here's what our ``models.py`` file should look like after this step: .. literalinclude:: src/models/tutorial/models.py - :lines: 5 :linenos: :language: py -Then, we'll add a ``Page`` class. Because this is a SQLAlchemy -application, this class should inherit from an instance of -:class:`sqlalchemy.ext.declarative.declarative_base`. Declarative -SQLAlchemy models are easier to use than directly-mapped ones. +The first thing we've done is to do is remove the stock ``MyModel`` class +from the generated ``models.py`` file. The ``MyModel`` class is only a +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`. .. literalinclude:: src/models/tutorial/models.py :pyobject: Page @@ -54,24 +50,18 @@ 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. -We'll also remove our ``populate`` function. We'll inline the populate step -into ``initialize_sql``, changing our ``initialize_sql`` function to add a -FrontPage object to our database at startup time. +Changing ``scripts/populate.py`` +-------------------------------- -.. literalinclude:: src/models/tutorial/models.py - :pyobject: initialize_sql - :linenos: - :language: python +We haven't looked at the guts of this file yet, but within the ``scripts`` +directory of your ``tutorial`` package is a file named ``populate.py``. Code +in this file is executed whenever we run the ``populate_tutorial`` command +(as we did in the installation step of this tutorial). -Here, we're using a slightly different binding syntax. It is otherwise -largely the same as the ``initialize_sql`` in the pcreate-generated -``models.py``. - -Our ``DBSession`` assignment stays the same as the original generated -``models.py``. - -Looking at the Result of all Our Edits to ``models.py`` -------------------------------------------------------- +Since we've changed our model, we need to make changes to our ``populate.py`` +script. In particular, we'll replace our import of ``MyModel`` with one of +``Page`` and we'll change the very end of the script to create a ``Page`` +rather than a ``MyModel`` and add it to our ``DBSession``. The result of all of our edits to ``models.py`` will end up looking something like this: @@ -80,6 +70,53 @@ something like this: :linenos: :language: python +Repopulating the Database +------------------------- + +Because our model has changed, in order to repopulate the database, we need +to rerun the ``populate_tutorial`` command to pick up the changes you've made +to both the models.py file and to the populate.py file. From the root of the +``tutorial`` project, directory execute the following commands. + +On UNIX: + +.. code-block:: text + + $ ../bin/populate_tutorial development.ini + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\populate_tutorial development.ini + +Success will look something like this:: + + 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread] + PRAGMA table_info("pages") + 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread] + CREATE TABLE pages ( + id INTEGER NOT NULL, + name TEXT, + data TEXT, + PRIMARY KEY (id), + UNIQUE (name) + ) + + + 2011-11-27 01:22:45,278 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2011-11-27 01:22:45,397 INFO [sqlalchemy.engine.base.Engine][MainThread] + COMMIT + 2011-11-27 01:22:45,400 INFO [sqlalchemy.engine.base.Engine][MainThread] + BEGIN (implicit) + 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread] + INSERT INTO pages (name, data) VALUES (?, ?) + 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread] + ('FrontPage', 'This is the front page') + 2011-11-27 01:22:45,402 INFO [sqlalchemy.engine.base.Engine][MainThread] + COMMIT + Viewing the Application in a Browser ------------------------------------ diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 21b97f7aa..7f533b635 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -2,34 +2,19 @@ Defining Views ============== -A :term:`view callable` in a :term:`url dispatch` -based :app:`Pyramid` -application is typically a simple Python function that accepts a single -parameter named :term:`request`. A view callable is assumed to return a -:term:`response` object. - -.. note:: - - A :app:`Pyramid` view can also be defined as callable - which accepts *two* arguments: a :term:`context` and a - :term:`request`. You'll see this two-argument pattern used in - other :app:`Pyramid` tutorials and applications. Either calling - convention will work in any :app:`Pyramid` application; the - calling conventions can be used interchangeably as necessary. In - :term:`url dispatch` based applications, however, the context - object is rarely used in the view body itself, so within this - tutorial we define views as callables that accept only a request to - avoid the visual "noise". If you do need the ``context`` within a - view function that only takes the request as a single argument, you - can obtain it via ``request.context``. - -The request passed to every view that is called as the result of a route -match has an attribute named ``matchdict`` that contains the elements placed -into the URL by the ``pattern`` of a ``route`` statement. For instance, if a -call to :meth:`pyramid.config.Configurator.add_route` in ``__init__.py`` had -the pattern ``{one}/{two}``, and the URL at ``http://example.com/foo/bar`` -was invoked, matching this pattern, the ``matchdict`` dictionary attached to -the request passed to the view would have a ``'one'`` key with the value -``'foo'`` and a ``'two'`` key with the value ``'bar'``. +A :term:`view callable` in a :app:`Pyramid` application is typically a simple +Python function that accepts a single parameter named :term:`request`. A +view callable is assumed to return a :term:`response` object. + +The request object passed to every view that is called as the result of a +route match has an attribute named ``matchdict`` that contains the elements +placed into the URL by the ``pattern`` of a ``route`` statement. For +instance, if a call to :meth:`pyramid.config.Configurator.add_route` in +``__init__.py`` had the pattern ``{one}/{two}``, and the URL at +``http://example.com/foo/bar`` was invoked, matching this pattern, the +``matchdict`` dictionary attached to the request passed to the view would +have a ``'one'`` key with the value ``'foo'`` and a ``'two'`` key with the +value ``'bar'``. The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/ @@ -52,24 +37,56 @@ Our resulting ``setup.py`` should look like so: :linenos: :language: python -.. note:: After these new dependencies are added, you will need to - rerun ``python setup.py develop`` inside the root of the - ``tutorial`` package to obtain and register the newly added - dependency package. +Running ``setup.py develop`` +============================ + +Since a new software dependency was added, you will need to rerun ``python +setup.py develop`` inside the root of the ``tutorial`` package to obtain and +register the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which setup.py lives) and execute the following command. + +On UNIX: + +.. code-block:: text + + $ cd tutorial + $ ../bin/python setup.py develop -Adding View Functions -===================== +On Windows: -We'll get rid of our ``my_view`` view function in our ``views.py`` file. -It's only an example and isn't relevant to our application. +.. code-block:: text -Then we're going to add four :term:`view callable` functions to our -``views.py`` module. One view callable (named ``view_wiki``) will display -the wiki itself (it will answer on the root URL), another named ``view_page`` -will display an individual page, another named ``add_page`` will allow a page -to be added, and a final view callable named ``edit_page`` will allow a page -to be edited. We'll describe each one briefly and show the resulting -``views.py`` file afterward. + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> ..\Scripts\python setup.py develop + +Success executing this command will end with a line to the console something +like:: + + Finished processing dependencies for tutorial==0.0 + +Changing the ``views.py`` File +============================== + +We're going to edit our ``views.py`` in a rather major way. The result of +all of our edits to ``views.py`` will leave it looking like this: + +.. literalinclude:: src/views/tutorial/views.py + :linenos: + :language: python + +We've gotten rid of the ``my_view`` view function and its decorator that was +added when we originally rendered the ``alchemy`` scaffold. It was only an +example and isn't relevant to our application. + +Then we added four :term:`view callable` functions to our ``views.py`` +module. One view callable (named ``view_wiki``) will display the wiki itself +(it will answer on the root URL), another named ``view_page`` will display an +individual page, another named ``add_page`` will allow a page to be added, +and a final view callable named ``edit_page`` will allow a page to be edited. +We'll describe each one briefly and show the resulting ``views.py`` file +afterward. .. note:: @@ -195,16 +212,6 @@ If the view execution *is* a result of a form submission (if the expression attribute of the page object. It then redirects to the ``view_page`` view of the wiki page. -Viewing the Result of all Our Edits to ``views.py`` -=================================================== - -The result of all of our edits to ``views.py`` will leave it looking -like this: - -.. literalinclude:: src/views/tutorial/views.py - :linenos: - :language: python - Adding Templates ================ @@ -270,47 +277,41 @@ subdirectories) and are just referred to by URL or by using the convenience method ``static_url`` e.g. ``request.static_url('{{package}}:static/foo.css')`` within templates. -Mapping Views to URLs in ``__init__.py`` -======================================== +Adding Routes to ``__init__.py`` +================================ The ``__init__.py`` file contains -:meth:`pyramid.config.Configurator.add_view` calls which serve to map -routes via :term:`url dispatch` to views. First, we’ll get rid of the -existing route created by the template using the name ``'home'``. It’s only an -example and isn’t relevant to our application. +:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes +to our application. First, we’ll get rid of the existing route created by +the template using the name ``'home'``. It’s only an example and isn’t +relevant to our application. We then need to add four calls to ``add_route``. Note that the *ordering* of these declarations is very important. ``route`` declarations are matched in the order they're found in the ``__init__.py`` file. #. Add a declaration which maps the pattern ``/`` (signifying the root URL) - to the route named ``view_wiki``. + to the route named ``view_wiki``. It maps to our ``view_wiki`` view + callable by virtue of the ``@view_config`` attached to the ``view_wiki`` + view function indicating ``route_name='view_wiki'``. #. Add a declaration which maps the pattern ``/{pagename}`` to the route named - ``view_page``. This is the regular view for a page. + ``view_page``. This is the regular view for a page. It maps + to our ``view_page`` view callable by virtue of the ``@view_config`` + attached to the ``view_page`` view function indicating + ``route_name='view_page'``. #. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. + route named ``add_page``. This is the add view for a new page. It maps + to our ``add_page`` view callable by virtue of the ``@view_config`` + attached to the ``add_page`` view function indicating + ``route_name='add_page'``. #. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. - -After we've defined the routes for our application, we can register views -to handle the processing and rendering that needs to happen when each route is -requested. - -#. Add a declaration which maps the ``view_wiki`` route to the view named - ``view_wiki`` in our ``views.py`` file. This is the :term:`default view` - for the wiki. - -#. Add a declaration which maps the ``view_page`` route to the view named - ``view_page`` in our ``views.py`` file. - -#. Add a declaration which maps the ``add_page`` route to the view named - ``add_page`` in our ``views.py`` file. - -#. Add a declaration which maps the ``edit_page`` route to the view named - ``edit_page`` in our ``views.py`` file. + route named ``edit_page``. This is the edit view for a page. It maps + to our ``edit_page`` view callable by virtue of the ``@view_config`` + attached to the ``edit_page`` view function indicating + ``route_name='edit_page'``. As a result of our edits, the ``__init__.py`` file should look something like so: diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index f9f5c4fba..be97f1cb3 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -40,13 +40,6 @@ Preparation, UNIX $ bin/easy_install pyramid -#. Use ``easy_install`` to install various packages from PyPI. - - .. code-block:: text - - $ bin/easy_install docutils nose coverage zope.sqlalchemy \ - SQLAlchemy pyramid_tm - Preparation, Windows -------------------- @@ -69,14 +62,6 @@ Preparation, Windows c:\pyramidtut> Scripts\easy_install pyramid -#. Use ``easy_install`` to install various packages from PyPI. - - .. code-block:: text - - c:\pyramidtut> Scripts\easy_install docutils \ - nose coverage zope.sqlalchemy SQLAlchemy pyramid_tm - - .. _sql_making_a_project: Making a Project @@ -84,7 +69,7 @@ Making a Project Your next step is to create a project. :app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We will use the -``pyramid_routesalchemy`` scaffold, which generates an application +``alchemy`` scaffold, which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. The below instructions assume your current working directory is the @@ -94,20 +79,27 @@ On UNIX: .. code-block:: text - $ bin/pcreate -s routesalchemy tutorial + $ bin/pcreate -s alchemy tutorial On Windows: .. code-block:: text - c:\pyramidtut> Scripts\pcreate -s routesalchemy tutorial + c:\pyramidtut> Scripts\pcreate -s alchemy tutorial -.. note:: If you are using Windows, the ``pyramid_routesalchemy`` +.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal gracefully with installation into a location that contains spaces in the path. If you experience startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. +Success executing this command will end with a line to the console something +like:: + + Please run the "populate_tutorial" script to set up the SQL + database before starting the application (e.g. + "$myvirtualenv/bin/populate_tutorial development.ini".) + Installing the Project in "Development Mode" ============================================ @@ -131,6 +123,11 @@ On Windows: c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> ..\Scripts\python setup.py develop +Success executing this command will end with a line to the console something +like:: + + Finished processing dependencies for tutorial==0.0 + .. _sql_running_tests: Running the Tests @@ -151,22 +148,13 @@ On Windows: c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q -Starting the Application -======================== - -Start the application. +For a successful test run, you should see output like this:: -On UNIX: - -.. code-block:: text - - $ ../bin/pserve development.ini --reload - -On Windows: - -.. code-block:: text - - c:\pyramidtut\tutorial> ..\Scripts\pserve development.ini --reload + . + ---------------------------------------------------------------------- + Ran 1 test in 0.094s + + OK Exposing Test Coverage Information ================================== @@ -208,24 +196,142 @@ On Windows: c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ --cover-erase --with-coverage -Looks like our package's ``models`` module doesn't quite have 100% -test coverage. +If successful, you will see output something like this:: -Visit the Application in a Browser -================================== + . + Name Stmts Miss Cover Missing + ------------------------------------------------ + tutorial 11 7 36% 9-15 + tutorial.models 17 0 100% + tutorial.scripts 0 0 100% + tutorial.tests 24 0 100% + tutorial.views 6 0 100% + ------------------------------------------------ + TOTAL 58 7 88% + ---------------------------------------------------------------------- + Ran 1 test in 0.459s + + OK + +Looks like our package doesn't quite have 100% test coverage. + +Starting the Application +======================== + +Start the application. + +On UNIX: + +.. code-block:: text + + $ ../bin/pserve development.ini --reload + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\pserve development.ini --reload + +If successful, you will see something like this on your console:: + + Starting subprocess with file monitor + Starting server in PID 8966. + Starting HTTP server on http://0.0.0.0:6543 + +This means the server is ready to accept requests. + +Populating the Database +======================= + +In a web browser, visit ``http://localhost:6543/``. + +You will see an error page with a title something like this:: + + sqlalchemy.exc.OperationalError + + OperationalError: (OperationalError) no such table: models ... + +Oh no! Something isn't working! + +This happens because we haven't populated the SQL database with any table +information yet. We need to use the ``populate_tutorial`` :term:`console +script` to populate our database before we can see the page render correctly. + +Stop the running Pyramid application by pressing ``ctrl-C`` in the console. +Make sure you're still in the ``tutorial`` directory (the directory with a +``development.ini`` in it) and type the following command: + +On UNIX: + +.. code-block:: text + + $ ../bin/populate_tutorial development.ini + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\populate_tutorial development.ini + +The output to your console should be something like this:: + + 2011-11-26 14:42:25,012 INFO [sqlalchemy.engine.base.Engine][MainThread] + PRAGMA table_info("models") + 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread] + CREATE TABLE models ( + id INTEGER NOT NULL, + name VARCHAR(255), + value INTEGER, + PRIMARY KEY (id), + UNIQUE (name) + ) + 2011-11-26 14:42:25,013 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2011-11-26 14:42:25,135 INFO [sqlalchemy.engine.base.Engine][MainThread] + COMMIT + 2011-11-26 14:42:25,137 INFO [sqlalchemy.engine.base.Engine][MainThread] + BEGIN (implicit) + 2011-11-26 14:42:25,138 INFO [sqlalchemy.engine.base.Engine][MainThread] + INSERT INTO models (name, value) VALUES (?, ?) + 2011-11-26 14:42:25,139 INFO [sqlalchemy.engine.base.Engine][MainThread] + (u'one', 1) + 2011-11-26 14:42:25,140 INFO [sqlalchemy.engine.base.Engine][MainThread] + COMMIT + +Success! You should now have a ``tutorial.db`` file in your current working +directory. This will be a SQLite database with a single table defined in it +(``models``). + +Starting the Application (Again) +================================ + +Start the application again. + +On UNIX: + +.. code-block:: text + + $ ../bin/pserve development.ini --reload + +On Windows: + +.. code-block:: text + + c:\pyramidtut\tutorial> ..\Scripts\pserve development.ini --reload -In a browser, visit ``http://localhost:6543/``. You will see the -generated application's default page. +At this point, when you visit ``http://localhost:6543/`` in your web browser, +you will no longer see an error; instead you will see the generated +application's default page. One thing you'll notice is the "debug toolbar" icon on right hand side of the page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. -Decisions the ``pyramid_routesalchemy`` Scaffold Has Made For You +Decisions the ``alchemy`` Scaffold Has Made For You ================================================================= -Creating a project using the ``pyramid_routesalchemy`` scaffold makes +Creating a project using the ``alchemy`` scaffold makes the following assumptions: - you are willing to use :term:`SQLAlchemy` as a database access tool diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt index d41f7f90f..6f851e9b7 100644 --- a/docs/tutorials/wiki2/src/authorization/README.txt +++ b/docs/tutorials/wiki2/src/authorization/README.txt @@ -1,4 +1 @@ tutorial README - - - diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index d1e262324..4f7493cba 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -19,7 +20,7 @@ port = 6543 # Begin logging configuration [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -31,6 +32,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index ac02acf3f..53eaf20a1 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 439a86923..09769bfff 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -42,6 +42,7 @@ setup(name='tutorial', entry_points = """\ [paste.app_factory] main = tutorial:main + [console_scripts] + populate_tutorial = tutorial.scripts.populate:main """, ) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index cca52fdfe..04dd5fe82 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -4,14 +4,15 @@ from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql from tutorial.security import groupfinder +from .models import DBSession + def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder) authz_policy = ACLAuthorizationPolicy() @@ -19,27 +20,13 @@ def main(global_config, **settings): root_factory='tutorial.models.RootFactory', authentication_policy=authn_policy, authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') config.add_route('logout', '/logout') config.add_route('view_page', '/{pagename}') config.add_route('add_page', '/add_page/{pagename}') config.add_route('edit_page', '/{pagename}/edit_page') - - config.add_view('tutorial.views.view_wiki', route_name='view_wiki') - config.add_view('tutorial.login.login', route_name='login', - renderer='tutorial:templates/login.pt') - config.add_view('tutorial.login.logout', route_name='logout') - config.add_view('tutorial.views.view_page', route_name='view_page', - renderer='tutorial:templates/view.pt') - config.add_view('tutorial.views.add_page', route_name='add_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.views.edit_page', route_name='edit_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.login.login', - context='pyramid.httpexceptions.HTTPForbidden', - renderer='tutorial:templates/login.pt') + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py deleted file mode 100644 index 5a825d8d6..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/login.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.httpexceptions import HTTPFound -from pyramid.security import remember -from pyramid.security import forget - -from tutorial.security import USERS - -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py index 832545cb1..c3bdcbea5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models.py @@ -1,17 +1,20 @@ -import transaction +from pyramid.security import ( + Allow, + Everyone, + ) -from pyramid.security import Allow -from pyramid.security import Everyone +from sqlalchemy import ( + Column, + Integer, + Text, + ) -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension @@ -29,20 +32,6 @@ class Page(Base): self.name = name self.data = data -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'This is the front page') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - transaction.abort() - class RootFactory(object): __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'group:editors', 'edit') ] diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/populate.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/populate.py new file mode 100644 index 000000000..981adff38 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/populate.py @@ -0,0 +1,35 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + Page, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv, settings=None): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py index cfd13071e..d88c9c71f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -5,4 +5,3 @@ GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, []) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index 332031ba4..31d2dc6d5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -1,15 +1,20 @@ import unittest - +import transaction from pyramid import testing def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base from sqlalchemy import create_engine + from tutorial.models import ( + DBSession, + Page, + Base + ) engine = create_engine('sqlite://') - DBSession.configure(bind=engine) - Base.metadata.bind = engine Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) return DBSession def _registerRoutes(config): @@ -20,14 +25,16 @@ def _registerRoutes(config): class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() + self.session = _initTestingDB() def tearDown(self): + self.session.remove() testing.tearDown() def _callFUT(self, request): from tutorial.views import view_wiki return view_wiki(request) - + def test_it(self): _registerRoutes(self.config) request = testing.DummyRequest() @@ -40,6 +47,7 @@ class ViewPageTests(unittest.TestCase): self.config = testing.setUp() def tearDown(self): + self.session.remove() testing.tearDown() def _callFUT(self, request): @@ -121,7 +129,8 @@ class EditPageTests(unittest.TestCase): self.session.add(page) info = self._callFUT(request) self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], 'http://example.com/abc/edit_page') + self.assertEqual(info['save_url'], + 'http://example.com/abc/edit_page') def test_it_submitted(self): from tutorial.models import Page diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index fc85d4585..375f1f5a5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -1,20 +1,36 @@ import re - from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound, HTTPNotFound -from pyramid.security import authenticated_userid +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + HTTPForbidden, + ) + +from pyramid.view import view_config + +from pyramid.security import ( + remember, + forget, + authenticated_userid, + ) -from tutorial.models import DBSession -from tutorial.models import Page +from .models import ( + DBSession, + Page, + ) + +from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") +@view_config(route_name='view_wiki') def view_wiki(request): return HTTPFound(location = request.route_url('view_page', pagename='FrontPage')) +@view_config(route_name='view_page', renderer='templates/view.pt') def view_page(request): pagename = request.matchdict['pagename'] session = DBSession() @@ -35,10 +51,11 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - logged_in = authenticated_userid(request) return dict(page=page, content=content, edit_url=edit_url, - logged_in=logged_in) + logged_in=authenticated_userid(request)) +@view_config(route_name='add_page', renderer='templates/edit.pt', + permission='edit') def add_page(request): name = request.matchdict['pagename'] if 'form.submitted' in request.params: @@ -50,9 +67,11 @@ def add_page(request): pagename=name)) save_url = request.route_url('add_page', pagename=name) page = Page('', '') - logged_in = authenticated_userid(request) - return dict(page=page, save_url=save_url, logged_in=logged_in) + return dict(page=page, save_url=save_url, + logged_in=authenticated_userid(request)) +@view_config(route_name='edit_page', renderer='templates/edit.pt', + permission='edit') def edit_page(request): name = request.matchdict['pagename'] session = DBSession() @@ -62,10 +81,43 @@ def edit_page(request): session.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) - - logged_in = authenticated_userid(request) return dict( page=page, save_url = request.route_url('edit_page', pagename=name), - logged_in = logged_in, + logged_in=authenticated_userid(request), ) + +@view_config(route_name='login', renderer='templates/login.pt') +@view_config(context=HTTPForbidden, renderer='templates/login.pt') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) + diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt index d41f7f90f..6f851e9b7 100644 --- a/docs/tutorials/wiki2/src/basiclayout/README.txt +++ b/docs/tutorials/wiki2/src/basiclayout/README.txt @@ -1,4 +1 @@ tutorial README - - - diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index d1e262324..4f7493cba 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -19,7 +20,7 @@ port = 6543 # Begin logging configuration [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -31,6 +32,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index ac02acf3f..53eaf20a1 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 3ab493912..0ca918cab 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -41,6 +41,8 @@ setup(name='tutorial', entry_points = """\ [paste.app_factory] main = tutorial:main + [console_scripts] + populate_tutorial = tutorial.scripts.populate:main """, ) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index b4038de3c..253341563 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -1,18 +1,16 @@ from pyramid.config import Configurator from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql +from .models import DBSession def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') - config.add_view('tutorial.views.my_view', route_name='home', - renderer='templates/mytemplate.pt') + config.scan() return config.make_wsgi_app() - diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py index 9b687931b..b6ac15429 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py @@ -1,43 +1,28 @@ -import transaction +from sqlalchemy import ( + Column, + Integer, + Text, + ) -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Unicode - -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension -DBSession = scoped_session(sessionmaker( - extension=ZopeTransactionExtension())) +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) Base = declarative_base() class MyModel(Base): __tablename__ = 'models' id = Column(Integer, primary_key=True) - name = Column(Unicode(255), unique=True) + name = Column(Text, unique=True) value = Column(Integer) def __init__(self, name, value): self.name = name self.value = value -def populate(): - session = DBSession() - model = MyModel(name=u'root',value=55) - session.add(model) - session.flush() - transaction.commit() - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - populate() - except IntegrityError: - transaction.abort() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/populate.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/populate.py new file mode 100644 index 000000000..0e828465f --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/populate.py @@ -0,0 +1,35 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + MyModel, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=1) + DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt index 14b88d16a..fbfa9870b 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> <![endif]--> diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py index 5efa6affa..653d061e4 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py @@ -1,23 +1,32 @@ import unittest +import transaction + from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import initialize_sql - session = initialize_sql(create_engine('sqlite://')) - return session +from .models import DBSession class TestMyView(unittest.TestCase): def setUp(self): self.config = testing.setUp() - _initTestingDB() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) def tearDown(self): + DBSession.remove() testing.tearDown() def test_it(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) - self.assertEqual(info['root'].name, 'root') + self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py index e550e3257..3e6abf2c2 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py @@ -1,7 +1,11 @@ -from tutorial.models import DBSession -from tutorial.models import MyModel +from pyramid.view import view_config +from .models import ( + DBSession, + MyModel, + ) + +@view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): - dbsession = DBSession() - root = dbsession.query(MyModel).filter(MyModel.name==u'root').first() - return {'root':root, 'project':'tutorial'} + one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + return {'one':one, 'project':'tutorial'} diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt index d41f7f90f..6f851e9b7 100644 --- a/docs/tutorials/wiki2/src/models/README.txt +++ b/docs/tutorials/wiki2/src/models/README.txt @@ -1,4 +1 @@ tutorial README - - - diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index d1e262324..4f7493cba 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -19,7 +20,7 @@ port = 6543 # Begin logging configuration [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -31,6 +32,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index ac02acf3f..53eaf20a1 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index 3ab493912..0ca918cab 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -41,6 +41,8 @@ setup(name='tutorial', entry_points = """\ [paste.app_factory] main = tutorial:main + [console_scripts] + populate_tutorial = tutorial.scripts.populate:main """, ) diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index fda7c9ce6..253341563 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -1,16 +1,16 @@ from pyramid.config import Configurator from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql +from .models import DBSession def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') - config.add_view('tutorial.views.my_view', route_name='home', - renderer='templates/mytemplate.pt') + config.scan() return config.make_wsgi_app() + diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py index 30f77a0b9..499396c5b 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models.py @@ -1,19 +1,19 @@ -import transaction +from sqlalchemy import ( + Column, + Integer, + Text, + ) -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension -DBSession = scoped_session(sessionmaker( - extension=ZopeTransactionExtension())) +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) Base = declarative_base() class Page(Base): @@ -27,16 +27,3 @@ class Page(Base): self.name = name self.data = data -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'This is the front page') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - transaction.abort() diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/populate.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/populate.py new file mode 100644 index 000000000..03188e8ad --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/populate.py @@ -0,0 +1,35 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + Page, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt index 14b88d16a..fbfa9870b 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> <![endif]--> diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py index 71f5e21e3..653d061e4 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py @@ -1,22 +1,32 @@ import unittest +import transaction + from pyramid import testing -def _initTestingDB(): - from tutorial.models import initialize_sql - session = initialize_sql('sqlite://') - return session +from .models import DBSession class TestMyView(unittest.TestCase): def setUp(self): self.config = testing.setUp() - _initTestingDB() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) def tearDown(self): + DBSession.remove() testing.tearDown() def test_it(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) - self.assertEqual(info['root'].name, 'root') + self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views.py index e550e3257..3e6abf2c2 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/views.py +++ b/docs/tutorials/wiki2/src/models/tutorial/views.py @@ -1,7 +1,11 @@ -from tutorial.models import DBSession -from tutorial.models import MyModel +from pyramid.view import view_config +from .models import ( + DBSession, + MyModel, + ) + +@view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): - dbsession = DBSession() - root = dbsession.query(MyModel).filter(MyModel.name==u'root').first() - return {'root':root, 'project':'tutorial'} + one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + return {'one':one, 'project':'tutorial'} diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt index d41f7f90f..6f851e9b7 100644 --- a/docs/tutorials/wiki2/src/tests/README.txt +++ b/docs/tutorials/wiki2/src/tests/README.txt @@ -1,4 +1 @@ tutorial README - - - diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index d1e262324..4f7493cba 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -19,7 +20,7 @@ port = 6543 # Begin logging configuration [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -31,6 +32,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index ac02acf3f..53eaf20a1 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index 6de8a1fbe..f965ccc6e 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -43,6 +43,7 @@ setup(name='tutorial', entry_points = """\ [paste.app_factory] main = tutorial:main + [console_scripts] + populate_tutorial = tutorial.scripts.populate:main """, ) - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index cca52fdfe..04dd5fe82 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -4,14 +4,15 @@ from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql from tutorial.security import groupfinder +from .models import DBSession + def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder) authz_policy = ACLAuthorizationPolicy() @@ -19,27 +20,13 @@ def main(global_config, **settings): root_factory='tutorial.models.RootFactory', authentication_policy=authn_policy, authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) - + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') config.add_route('logout', '/logout') config.add_route('view_page', '/{pagename}') config.add_route('add_page', '/add_page/{pagename}') config.add_route('edit_page', '/{pagename}/edit_page') - - config.add_view('tutorial.views.view_wiki', route_name='view_wiki') - config.add_view('tutorial.login.login', route_name='login', - renderer='tutorial:templates/login.pt') - config.add_view('tutorial.login.logout', route_name='logout') - config.add_view('tutorial.views.view_page', route_name='view_page', - renderer='tutorial:templates/view.pt') - config.add_view('tutorial.views.add_page', route_name='add_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.views.edit_page', route_name='edit_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.login.login', - context='pyramid.httpexceptions.HTTPForbidden', - renderer='tutorial:templates/login.pt') + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/login.py b/docs/tutorials/wiki2/src/tests/tutorial/login.py deleted file mode 100644 index 5a825d8d6..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/login.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.httpexceptions import HTTPFound -from pyramid.security import remember -from pyramid.security import forget - -from tutorial.security import USERS - -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py index 832545cb1..c3bdcbea5 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models.py @@ -1,17 +1,20 @@ -import transaction +from pyramid.security import ( + Allow, + Everyone, + ) -from pyramid.security import Allow -from pyramid.security import Everyone +from sqlalchemy import ( + Column, + Integer, + Text, + ) -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension @@ -29,20 +32,6 @@ class Page(Base): self.name = name self.data = data -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'This is the front page') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - transaction.abort() - class RootFactory(object): __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'group:editors', 'edit') ] diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/populate.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/populate.py new file mode 100644 index 000000000..de74f4d63 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/populate.py @@ -0,0 +1,36 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + Page, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv, settings=None): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + if settings is None: + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py index cfd13071e..d88c9c71f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -5,4 +5,3 @@ GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, []) - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py index 8439e2748..557d1b1be 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests.py @@ -1,16 +1,20 @@ import unittest - +import transaction from pyramid import testing - def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base from sqlalchemy import create_engine - engine = create_engine('sqlite:///:memory:') - DBSession.configure(bind=engine) - Base.metadata.bind = engine + from tutorial.models import ( + DBSession, + Page, + Base + ) + engine = create_engine('sqlite://') Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) return DBSession def _registerRoutes(config): @@ -39,28 +43,6 @@ class PageModelTests(unittest.TestCase): self.assertEqual(instance.name, 'SomeName') self.assertEqual(instance.data, 'some data') -class InitializeSqlTests(unittest.TestCase): - - def setUp(self): - from tutorial.models import DBSession - DBSession.remove() - - def tearDown(self): - from tutorial.models import DBSession - DBSession.remove() - - def _callFUT(self, engine): - from tutorial.models import initialize_sql - return initialize_sql(engine) - - def test_it(self): - from sqlalchemy import create_engine - engine = create_engine('sqlite:///:memory:') - self._callFUT(engine) - from tutorial.models import DBSession, Page - self.assertEqual(DBSession.query(Page).one().data, - 'This is the front page') - class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -191,10 +173,11 @@ class FunctionalTests(unittest.TestCase): def setUp(self): from tutorial import main - settings = { 'sqlalchemy.url': 'sqlite:///:memory:'} + settings = { 'sqlalchemy.url': 'sqlite://'} app = main({}, **settings) from webtest import TestApp self.testapp = TestApp(app) + _initTestingDB() def tearDown(self): del self.testapp @@ -263,3 +246,23 @@ class FunctionalTests(unittest.TestCase): self.testapp.get(self.editor_login, status=302) res = self.testapp.get('/FrontPage', status=200) self.assertTrue('FrontPage' in res.body) + +class Test_populate(unittest.TestCase): + def setUp(self): + from tutorial.models import DBSession + DBSession.remove() + + def tearDown(self): + from tutorial.models import DBSession + DBSession.remove() + + def _callFUT(self, settings): + from tutorial.scripts.populate import main + main(['foo', 'development.ini'], settings) + + def test_it(self): + self._callFUT({'sqlalchemy.url':'sqlite://'}) + from tutorial.models import DBSession, Page + self.assertEqual(DBSession.query(Page).one().data, + 'This is the front page') + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index fc85d4585..375f1f5a5 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -1,20 +1,36 @@ import re - from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound, HTTPNotFound -from pyramid.security import authenticated_userid +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + HTTPForbidden, + ) + +from pyramid.view import view_config + +from pyramid.security import ( + remember, + forget, + authenticated_userid, + ) -from tutorial.models import DBSession -from tutorial.models import Page +from .models import ( + DBSession, + Page, + ) + +from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") +@view_config(route_name='view_wiki') def view_wiki(request): return HTTPFound(location = request.route_url('view_page', pagename='FrontPage')) +@view_config(route_name='view_page', renderer='templates/view.pt') def view_page(request): pagename = request.matchdict['pagename'] session = DBSession() @@ -35,10 +51,11 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - logged_in = authenticated_userid(request) return dict(page=page, content=content, edit_url=edit_url, - logged_in=logged_in) + logged_in=authenticated_userid(request)) +@view_config(route_name='add_page', renderer='templates/edit.pt', + permission='edit') def add_page(request): name = request.matchdict['pagename'] if 'form.submitted' in request.params: @@ -50,9 +67,11 @@ def add_page(request): pagename=name)) save_url = request.route_url('add_page', pagename=name) page = Page('', '') - logged_in = authenticated_userid(request) - return dict(page=page, save_url=save_url, logged_in=logged_in) + return dict(page=page, save_url=save_url, + logged_in=authenticated_userid(request)) +@view_config(route_name='edit_page', renderer='templates/edit.pt', + permission='edit') def edit_page(request): name = request.matchdict['pagename'] session = DBSession() @@ -62,10 +81,43 @@ def edit_page(request): session.add(page) return HTTPFound(location = request.route_url('view_page', pagename=name)) - - logged_in = authenticated_userid(request) return dict( page=page, save_url = request.route_url('edit_page', pagename=name), - logged_in = logged_in, + logged_in=authenticated_userid(request), ) + +@view_config(route_name='login', renderer='templates/login.pt') +@view_config(context=HTTPForbidden, renderer='templates/login.pt') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) + diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt index d41f7f90f..6f851e9b7 100644 --- a/docs/tutorials/wiki2/src/views/README.txt +++ b/docs/tutorials/wiki2/src/views/README.txt @@ -1,4 +1 @@ tutorial README - - - diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index d1e262324..4f7493cba 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -19,7 +20,7 @@ port = 6543 # Begin logging configuration [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -31,6 +32,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index ac02acf3f..53eaf20a1 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -1,5 +1,6 @@ [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index 439a86923..9c0e88eb0 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -42,6 +42,8 @@ setup(name='tutorial', entry_points = """\ [paste.app_factory] main = tutorial:main + [console_scripts] + populate_tutorial = tutorial.scripts.populate:main """, ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 7d79f7a1f..b30d593cf 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -1,25 +1,18 @@ from pyramid.config import Configurator from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql +from .models import DBSession def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static', cache_max_age=3600) + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('view_page', '/{pagename}') config.add_route('add_page', '/add_page/{pagename}') config.add_route('edit_page', '/{pagename}/edit_page') - config.add_view('tutorial.views.view_wiki', route_name='view_wiki') - config.add_view('tutorial.views.view_page', route_name='view_page', - renderer='tutorial:templates/view.pt') - config.add_view('tutorial.views.add_page', route_name='add_page', - renderer='tutorial:templates/edit.pt') - config.add_view('tutorial.views.edit_page', route_name='edit_page', - renderer='tutorial:templates/edit.pt') + config.scan() return config.make_wsgi_app() - diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py index 30506f67e..499396c5b 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models.py @@ -1,14 +1,15 @@ -import transaction +from sqlalchemy import ( + Column, + Integer, + Text, + ) -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension @@ -26,16 +27,3 @@ class Page(Base): self.name = name self.data = data -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'initial data') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - transaction.abort() diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/populate.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/populate.py new file mode 100644 index 000000000..03188e8ad --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/populate.py @@ -0,0 +1,35 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + Page, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt index 14b88d16a..fbfa9870b 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> + <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> <![endif]--> diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index 668bf5479..31d2dc6d5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -1,15 +1,20 @@ import unittest - +import transaction from pyramid import testing def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base from sqlalchemy import create_engine + from tutorial.models import ( + DBSession, + Page, + Base + ) engine = create_engine('sqlite://') - DBSession.configure(bind=engine) - Base.metadata.bind = engine Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page('FrontPage', 'This is the front page') + DBSession.add(model) return DBSession def _registerRoutes(config): @@ -20,8 +25,10 @@ def _registerRoutes(config): class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() + self.session = _initTestingDB() def tearDown(self): + self.session.remove() testing.tearDown() def _callFUT(self, request): diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index e04b96ae4..5c49dd2e8 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -1,19 +1,26 @@ import re - from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) +from pyramid.view import view_config -from tutorial.models import DBSession -from tutorial.models import Page +from .models import ( + DBSession, + Page, + ) # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") +@view_config(route_name='view_wiki') def view_wiki(request): return HTTPFound(location = request.route_url('view_page', pagename='FrontPage')) +@view_config(route_name='view_page', renderer='templates/view.pt') def view_page(request): pagename = request.matchdict['pagename'] session = DBSession() @@ -36,6 +43,7 @@ def view_page(request): edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) +@view_config(route_name='add_page', renderer='templates/edit.pt') def add_page(request): name = request.matchdict['pagename'] if 'form.submitted' in request.params: @@ -49,6 +57,7 @@ def add_page(request): page = Page('', '') return dict(page=page, save_url=save_url) +@view_config(route_name='edit_page', renderer='templates/edit.pt') def edit_page(request): name = request.matchdict['pagename'] session = DBSession() diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index c120b1c61..25cf2c25e 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -13,9 +13,9 @@ We write a test class for the model class ``Page`` and another test class for the ``initialize_sql`` function. To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_routesalchemy`` project generator. We'll add two -test classes: one for the ``Page`` model named ``PageModelTests``, one for the -``initialize_sql`` function named ``InitializeSqlTests``. +result of the ``alchemy`` scaffold. We'll add two test classes: one for the +``Page`` model named ``PageModelTests``, and one for the ``populate`` script +named ``Test_populate``. Testing the Views ================= diff --git a/docs/whatsnew-1.0.rst b/docs/whatsnew-1.0.rst index 61d74a899..66cb9be3a 100644 --- a/docs/whatsnew-1.0.rst +++ b/docs/whatsnew-1.0.rst @@ -110,7 +110,7 @@ Scaffold Improvements (``starter``, ``routesalchemy``, ``alchemy``, ``zodb``) instead of ZCML configuration. -- The ``pyramid_zodb``, ``pyramid_routesalchemy`` and ``pyramid_alchemy`` +- The ``pyramid_zodb``, ``routesalchemy`` and ``pyramid_alchemy`` scaffolds now use a default "commit veto" hook when configuring the ``repoze.tm2`` transaction manager in ``development.ini``. This prevents a transaction from being committed when the response status code is within diff --git a/docs/whatsnew-1.2.rst b/docs/whatsnew-1.2.rst index 49854a7e7..ea56cf52d 100644 --- a/docs/whatsnew-1.2.rst +++ b/docs/whatsnew-1.2.rst @@ -63,7 +63,7 @@ Scaffolding Changes package at all; configuration in the ``production.ini`` file which used to require its ``error_catcher`` middleware has been removed. Configuring error catching / email sending is now the domain of the ``pyramid_exclog`` - package (see https://docs.pylonsproject.org/projects/pyramid_exclog/dev/). + package (see http://docs.pylonsproject.org/projects/pyramid_exclog/dev/). - All scaffolds now send the ``cache_max_age`` parameter to the ``add_static_view`` method. diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst new file mode 100644 index 000000000..28c161ad0 --- /dev/null +++ b/docs/whatsnew-1.3.rst @@ -0,0 +1,315 @@ +What's New In Pyramid 1.3 +========================= + +This article explains the new features in :app:`Pyramid` version 1.3 as +compared to its predecessor, :app:`Pyramid` 1.2. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.3, as well as software dependency changes and notable +documentation additions. + +Major Feature Additions +----------------------- + +The major feature additions in Pyramid 1.3 follow. + +Python 3 Compatibility +~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid is now Python 3 compatible. Python 3.2 or better is required. + +.. warning:: + + As of this writing (the release of Pyramid 1.3a1), if you attempt to + install a Pyramid project that used ``alchemy`` scaffold via ``setup.py + develop`` on Python 3.2, it may quit with an installation error while + trying to install ``Pygments``. If this happens, please rerun the + ``setup.py develop`` command again and it will complete. We're just as + clueless as you are as to why this happens at this point, but hopefully + we'll figure it out before Pyramid 1.3 leaves the alpha/beta phase. + +This feature required us to make some compromises. + +Pyramid no longer runs on Python 2.5. This includes the most recent release +of Jython and the Python 2.5 version of Google App Engine. We could not +easily "straddle" Python 2 and 3 versions and support Python 2 versions older +than Python 2.6. You will need Python 2.6 or better to run this version of +Pyramid. If you need to use Python 2.5, you should use the most recent 1.2.X +release of Pyramid. + +Though many Pyramid add-ons have releases which are already Python 3 +compatible (in particular ``pyramid_debugtoolbar``, ``pyramid_jinja2``, +``pyramid_exclog``, and ``pyramid_tm``), some are still known to work only +under Python 2. Likewise, some scaffolding dependencies (particularly ZODB) +do not yet work under Python +3. Please be patient as we gain full ecosystem support for Python 3. You +can see more details about ongoing porting efforts at +https://github.com/Pylons/pyramid/wiki/Python-3-Porting . + +The libraries named ``Paste`` and ``PasteScript`` which have been +dependencies of Pyramid since 1.0+ have not been ported to Python 3, and we +were unwilling to port and maintain them ourselves. As a result, we've had +to make some changes: + +- We've replaced the ``paster`` command with Pyramid-specific analogues. + +- We've made the default WSGI server the ``wsgiref`` server. + +Previously (in Pyramid 1.0, 1.1 and 1.2), you created a Pyramid application +using ``paster create``, like so:: + + $ myvenv/bin/paster create -t pyramid_starter foo + +You're now instead required to create an application using ``pcreate`` like +so:: + + $ myvenv/bin/pcreate -s starter foo + +Note that the names of available scaffolds have changed and the flags +supported by ``pcreate`` are different than those that were supported by +``paster create``. + +Instead of running a Pyramid project created via a scaffold using ``paster +serve``, as was done in Pyramid <= 1.2.X, you now must use the ``pserve`` +command:: + + $myvenv/bin/pserve development.ini + +The ``ini`` configuration file format supported by Pyramid has not changed. +As a result, Python 2-only users can install PasteScript manually and use +``paster serve`` and ``paster create`` instead if they like. However, using +``pserve`` and ``pcreate`` will work under both Python 2 and Python 3. + +Analogues of ``paster pshell``, ``paster pviews`` and ``paster ptweens`` also +exist under the respective console script names ``pshell``, ``pviews``, and +``ptweens``. + +We've replaced use of the Paste ``httpserver`` with the ``wsgiref`` server in +the scaffolds, so once you create a project from a scaffold, its +``development.ini`` and ``production.ini`` will have the following line:: + + use = egg:pyramid#wsgiref + +Instead of this (which was the default in older versions):: + + use = egg:Paste#http + +Using ``wsgiref`` as the default WSGI server is purely a default to make it +possible to use the same scaffolding under Python 2 and Python 3; people +running Pyramid under Python 2 can still manually install ``Paste`` and use +the Paste ``httpserver`` by replacing the former line with the latter. This is +actually recommended if you rely on proxying from Apache or Nginx to a +``pserve`` -invoked application. **The wsgiref server is not a production +quality server.** See :ref:`alternate_wsgi_server` for more information. + +.. warning:: + + Previously, paste.httpserver "helped" by converting header values that weren't + strings to strings. The wsgiref server, on the other hand implements the spec + more fully. This specifically may affect you if you are modifying headers on + your response. The following error might be an indicator of this problem: + **AssertionError: Header values must be strings, please check the type of + the header being returned.** A common case would be returning unicode headers + instead of string headers. + +A new :mod:`pyramid.compat` module was added which provides Python 2/3 +straddling support for Pyramid add-ons and development environments. + +Python 3 compatibility required dropping some package dependencies and +support for older Python versions and platforms. See the "Backwards +Incompatibilities" section below for more information. + +Introspection +~~~~~~~~~~~~~ + +A configuration introspection system was added; see +:ref:`using_introspection` and :ref:`introspection` for more information on +using the introspection system as a developer. + +The latest release of the pyramid debug toolbar (0.9.7+) provides an +"Introspection" panel that exposes introspection information to a Pyramid +application developer. + +New APIs were added to support introspection +:attr:`pyramid.registry.Introspectable`, +:attr:`pyramid.registry.noop_introspector`, +:attr:`pyramid.config.Configurator.introspector`, +:attr:`pyramid.config.Configurator.introspectable`, +:attr:`pyramid.registry.Registry.introspector`. + +``@view_defaults`` Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use a class as a view, you can use the new +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. + +For instance, if you've got a class that has methods that represent "REST +actions", all which are mapped to the same route, but different request +methods, instead of this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') + + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(request_method='GET') + def get(self): + return Response('get') + + @view_config(request_method='POST') + def post(self): + return Response('post') + + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +This also works for imperative view configurations that involve a class. + +See :ref:`view_defaults` for more information. + +Minor Feature Additions +----------------------- + +- New APIs: :class:`pyramid.path.AssetResolver` and + :class:`pyramid.path.DottedNameResolver`. The former can be used to + resolve an :term:`asset specification` to an API that can be used to read + the asset's data, the latter can be used to resolve a :term:`dotted Python + name` to a module or a package. + +- A ``mako.directories`` setting is no longer required to use Mako templates + Rationale: Mako template renderers can be specified using an absolute asset + spec. An entire application can be written with such asset specs, + requiring no ordered lookup path. + +- ``bpython`` interpreter compatibility in ``pshell``. See + :ref:`ipython_or_bpython` for more information. + +- Added :func:`pyramid.paster.get_appsettings` API function. This function + returns the settings defined within an ``[app:...]`` section in a + PasteDeploy ``ini`` file. + +- Added :func:`pyramid.paster.setup_logging` API function. This function + sets up Python logging according to the logging configuration in a + PasteDeploy ``ini`` file. + +- Configuration conflict reporting is reported in a more understandable way + ("Line 11 in file..." vs. a repr of a tuple of similar info). + +- We allow extra keyword arguments to be passed to the + :meth:`pyramid.config.Configurator.action` method. + +Backwards Incompatibilities +--------------------------- + +- Pyramid no longer runs on Python 2.5 (which includes the most recent + release of Jython and the Python 2.5 version of GAE as of this writing). + +- The ``paster`` command is no longer the documented way to create projects, + start the server, or run debugging commands. To create projects from + scaffolds, ``paster create`` is replaced by the ``pcreate`` console script. + To serve up a project, ``paster serve`` is replaced by the ``pserve`` + console script. New console scripts named ``pshell``, ``pviews``, + ``proutes``, and ``ptweens`` do what their ``paster <commandname>`` + equivalents used to do. All relevant narrative documentation has been + updated. Rationale: the Paste and PasteScript packages do not run under + Python 3. + +- The default WSGI server run as the result of ``pserve`` from newly rendered + scaffolding is now the ``wsgiref`` WSGI server instead of the + ``paste.httpserver`` server. ``wsgiref``, unlike the server it replaced + (``paste.httpserver``) is not a production quality server. See + :ref:`alternate_wsgi_server` for information about how to use another WSGI + server in production. Rationale: the Paste and PasteScript packages do not + run under Python 3. + +- The ``pshell`` command (see "paster pshell") no longer accepts a + ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` + or ``--python-shell`` argument, which can be any of the values ``python``, + ``ipython`` or ``bpython``. + +- Removed the ``pyramid.renderers.renderer_from_name`` function. It has been + deprecated since Pyramid 1.0, and was never an API. + +- To use ZCML with versions of Pyramid >= 1.3, you will need ``pyramid_zcml`` + version >= 0.8 and ``zope.configuration`` version >= 3.8.0. The + ``pyramid_zcml`` package version 0.8 is backwards compatible all the way to + Pyramid 1.0, so you won't be warned if you have older versions installed + and upgrade Pyramid itself "in-place"; it may simply break instead + (particularly if you use ZCML's ``includeOverrides`` directive). + +Documentation Enhancements +-------------------------- + +- The :ref:`bfg_sql_wiki_tutorial` has been updated. It now uses + ``@view_config`` decorators and an explicit database population script. + +- Minor updates to the :ref:`bfg_wiki_tutorial`. + +- A narrative documentation chapter named :ref:`extconfig_narr` was added; it + describes how to add a custom :term:`configuration directive`, and how use + the :meth:`pyramid.config.Configurator.action` method within custom + directives. It also describes how to add :term:`introspectable` objects. + +- A narrative documentation chapter named :ref:`using_introspection` was + added. It describes how to query the introspection system. + +Dependency Changes +------------------ + +- Pyramid no longer depends on the ``zope.component`` package, except as a + testing dependency. + +- Pyramid now depends on the following package versions: + zope.interface>=3.8.0, WebOb>=1.2dev, repoze.lru>=0.4, + zope.deprecation>=3.5.0, translationstring>=0.4 for Python 3 compatibility + purposes. It also, as a testing dependency, depends on WebTest>=1.3.1 for + the same reason. + +- Pyramid no longer depends on the ``Paste`` or ``PasteScript`` packages. + These packages are not Python 3 compatible. + +Scaffolding Changes +------------------- + +- Rendered scaffolds have now been changed to be more relocatable (fewer + mentions of the package name within files in the package). + +- The ``routesalchemy`` scaffold has been renamed ``alchemy``, replacing the + older (traversal-based) ``alchemy`` scaffold (which has been retired). + +- The ``alchemy`` and ``starter`` scaffolds are Python 3 compatible. + +- The ``starter`` scaffold now uses URL dispatch by default. diff --git a/pyramid/asset.py b/pyramid/asset.py index 4bf0d7bf4..e6a145341 100644 --- a/pyramid/asset.py +++ b/pyramid/asset.py @@ -3,8 +3,10 @@ import pkg_resources from pyramid.compat import string_types -from pyramid.path import package_path -from pyramid.path import package_name +from pyramid.path import ( + package_path, + package_name, + ) def resolve_asset_spec(spec, pname='__main__'): if pname and not isinstance(pname, string_types): @@ -30,7 +32,8 @@ def asset_spec_from_abspath(abspath, package): return '%s:%s' % (package_name(package), relpath.replace(os.path.sep, '/')) return abspath - + +# bw compat only; use pyramid.path.AssetDescriptor.abspath() instead def abspath_from_asset_spec(spec, pname='__main__'): if pname is None: return spec @@ -38,3 +41,4 @@ def abspath_from_asset_spec(spec, pname='__main__'): if pname is None: return filename return pkg_resources.resource_filename(pname, filename) + diff --git a/pyramid/authentication.py b/pyramid/authentication.py index ec4eb8f62..83bdb13d1 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -8,19 +8,27 @@ import time as time_mod from zope.interface import implementer -from pyramid.compat import long -from pyramid.compat import text_type -from pyramid.compat import binary_type -from pyramid.compat import url_unquote -from pyramid.compat import url_quote -from pyramid.compat import bytes_ -from pyramid.compat import ascii_native_ - -from pyramid.interfaces import IAuthenticationPolicy -from pyramid.interfaces import IDebugLogger - -from pyramid.security import Authenticated -from pyramid.security import Everyone +from pyramid.compat import ( + long, + text_type, + binary_type, + url_unquote, + url_quote, + bytes_, + ascii_native_, + ) + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IDebugLogger, + ) + +from pyramid.security import ( + Authenticated, + Everyone, + ) + +from pyramid.util import strings_differ VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") @@ -249,7 +257,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): @implementer(IAuthenticationPolicy) class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): """ A :app:`Pyramid` :term:`authentication policy` which - obtains data from an :class:`paste.auth.auth_tkt` cookie. + obtains data from a Pyramid "auth ticket" cookie. Constructor Arguments @@ -384,7 +392,8 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): return result['userid'] def remember(self, request, principal, **kw): - """ Accepts the following kw args: ``max_age``.""" + """ Accepts the following kw args: ``max_age=<int-seconds>, + ``tokens=<sequence-of-ascii-strings>``""" return self.cookie.remember(request, principal, **kw) def forget(self, request): @@ -409,26 +418,16 @@ class AuthTicket(object): Once you provide all the arguments, use .cookie_value() to generate the appropriate authentication ticket. - CGI usage:: + Usage:: - token = auth_tkt.AuthTick('sharedsecret', 'username', + token = AuthTicket('sharedsecret', 'username', os.environ['REMOTE_ADDR'], tokens=['admin']) - print 'Status: 200 OK' - print 'Content-type: text/html' - print token.cookie() - print - ... redirect HTML ... - - Webware usage:: + val = token.cookie_value() - token = auth_tkt.AuthTick('sharedsecret', 'username', - self.request().environ()['REMOTE_ADDR'], tokens=['admin']) - self.response().setCookie('auth_tkt', token.cookie_value()) """ def __init__(self, secret, userid, ip, tokens=(), user_data='', - time=None, cookie_name='auth_tkt', - secure=False): + time=None, cookie_name='auth_tkt', secure=False): self.secret = secret self.userid = userid self.ip = ip @@ -494,7 +493,9 @@ def parse_ticket(secret, ticket, ip): expected = calculate_digest(ip, timestamp, secret, userid, tokens, user_data) - if expected != digest: + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(expected, digest): raise BadTicket('Digest signature is not correct', expected=(expected, digest)) diff --git a/pyramid/authorization.py b/pyramid/authorization.py index b1ef10033..fc711e88b 100644 --- a/pyramid/authorization.py +++ b/pyramid/authorization.py @@ -3,11 +3,14 @@ from zope.interface import implementer from pyramid.interfaces import IAuthorizationPolicy from pyramid.location import lineage -from pyramid.security import ACLAllowed -from pyramid.security import ACLDenied -from pyramid.security import Allow -from pyramid.security import Deny -from pyramid.security import Everyone + +from pyramid.security import ( + ACLAllowed, + ACLDenied, + Allow, + Deny, + Everyone, + ) @implementer(IAuthorizationPolicy) class ACLAuthorizationPolicy(object): diff --git a/pyramid/compat.py b/pyramid/compat.py index 3ac235b0f..484cb7b97 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -13,14 +13,6 @@ try: except ImportError: # pragma: no cover import pickle -try: - import json -except ImportError: # pragma: no cover - try: - import simplejson as json - except NotImplementedError: - from django.utils import simplejson as json # GAE - # True if we are running on Python 3. PY3 = sys.version_info[0] == 3 @@ -40,11 +32,15 @@ else: long = long def text_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``binary_type``, return + ``s.encode(encoding, errors)``, otherwise return ``s``""" if isinstance(s, binary_type): return s.decode(encoding, errors) return s # pragma: no cover def bytes_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``s``""" if isinstance(s, text_type): return s.encode(encoding, errors) return s @@ -60,17 +56,38 @@ else: s = s.encode('ascii') return str(s) +ascii_native_.__doc__ = """ +Python 3: If ``s`` is an instance of ``text_type``, return +``s.encode('ascii')``, otherwise return ``str(s, 'ascii', 'strict')`` + +Python 2: If ``s`` is an instance of ``text_type``, return +``s.encode('ascii')``, otherwise return ``str(s)`` +""" + + if PY3: # pragma: no cover def native_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s``, otherwise return ``str(s, encoding, errors)``""" if isinstance(s, text_type): return s return str(s, encoding, errors) else: def native_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" if isinstance(s, text_type): return s.encode(encoding, errors) return str(s) +native_.__doc__ = """ +Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise +return ``str(s, encoding, errors)`` + +Python 2: If ``s`` is an instance of ``text_type``, return +``s.encode(encoding, errors)``, otherwise return ``str(s)`` +""" + if PY3: # pragma: no cover from urllib import parse urlparse = parse @@ -101,12 +118,13 @@ if PY3: # pragma: no cover def reraise(tp, value, tb=None): + if value is None: + value = tp if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value - print_ = getattr(builtins, "print") del builtins else: # pragma: no cover @@ -128,51 +146,6 @@ else: # pragma: no cover """) - def print_(*args, **kwargs): - """The new-style print function.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - def write(data): - if not isinstance(data, basestring): - data = str(data) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) - if PY3: # pragma: no cover def iteritems_(d): return d.items() @@ -234,4 +207,9 @@ try: from StringIO import StringIO as NativeIO except ImportError: # pragma: no cover from io import StringIO as NativeIO + +# "json" is not an API; it's here to support older pyramid_debugtoolbar +# versions which attempt to import it +import json + diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 40c3c037b..04f9b6fb5 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1,5 +1,6 @@ import inspect import logging +import operator import os import sys import types @@ -8,28 +9,53 @@ import venusian from webob.exc import WSGIHTTPException as WebobWSGIHTTPException -from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IExceptionResponse +from pyramid.interfaces import ( + IDebugLogger, + IExceptionResponse, + ) from pyramid.asset import resolve_asset_spec + from pyramid.authorization import ACLAuthorizationPolicy -from pyramid.compat import text_ -from pyramid.compat import reraise -from pyramid.compat import string_types -from pyramid.compat import PY3 + +from pyramid.compat import ( + text_, + reraise, + string_types, + PY3, + ) + from pyramid.events import ApplicationCreated -from pyramid.exceptions import ConfigurationConflictError -from pyramid.exceptions import ConfigurationError -from pyramid.exceptions import ConfigurationExecutionError + +from pyramid.exceptions import ( + ConfigurationConflictError, + ConfigurationError, + ConfigurationExecutionError, + ) + from pyramid.httpexceptions import default_exceptionresponse_view -from pyramid.path import caller_package -from pyramid.path import package_of -from pyramid.registry import Registry + +from pyramid.path import ( + caller_package, + package_of, + ) + +from pyramid.registry import ( + Introspectable, + Introspector, + Registry, + ) + from pyramid.router import Router + from pyramid.settings import aslist + from pyramid.threadlocal import manager -from pyramid.util import DottedNameResolver -from pyramid.util import WeakOrderedSet + +from pyramid.util import ( + WeakOrderedSet, + object_description, + ) from pyramid.config.adapters import AdaptersConfiguratorMixin from pyramid.config.assets import AssetsConfiguratorMixin @@ -42,11 +68,17 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + ActionInfo, + ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin +from pyramid.path import DottedNameResolver + empty = text_('') +_marker = object() ConfigurationError = ConfigurationError # pyflakes @@ -205,14 +237,22 @@ class Configurator( If ``route_prefix`` is passed, all routes added with :meth:`pyramid.config.Configurator.add_route` will have the specified path - prepended to their pattern. This parameter is new in Pyramid 1.2.""" + prepended to their pattern. This parameter is new in Pyramid 1.2. + If ``introspector`` is passed, it must be an instance implementing the + attributes and methods of :class:`pyramid.interfaces.IIntrospector`. If + ``introspector`` is not passed (or is passed as ``None``), the default + introspector implementation will be used. This parameter is new in + Pyramid 1.3. + """ manager = manager # for testing injection venusian = venusian # for testing injection _ainfo = None basepath = None includepath = () info = '' + object_description = staticmethod(object_description) + introspectable = Introspectable def __init__(self, registry=None, @@ -232,13 +272,14 @@ class Configurator( autocommit=False, exceptionresponse_view=default_exceptionresponse_view, route_prefix=None, + introspector=None, ): if package is None: package = caller_package() name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver - self.package_name = name_resolver.package_name - self.package = name_resolver.package + self.package_name = name_resolver.get_package_name() + self.package = name_resolver.get_package() self.registry = registry self.autocommit = autocommit self.route_prefix = route_prefix @@ -259,15 +300,24 @@ class Configurator( session_factory=session_factory, default_view_mapper=default_view_mapper, exceptionresponse_view=exceptionresponse_view, + introspector=introspector, ) - def setup_registry(self, settings=None, root_factory=None, - authentication_policy=None, authorization_policy=None, - renderers=None, debug_logger=None, - locale_negotiator=None, request_factory=None, - renderer_globals_factory=None, default_permission=None, - session_factory=None, default_view_mapper=None, - exceptionresponse_view=default_exceptionresponse_view): + def setup_registry(self, + settings=None, + root_factory=None, + authentication_policy=None, + authorization_policy=None, + renderers=None, + debug_logger=None, + locale_negotiator=None, + request_factory=None, + renderer_globals_factory=None, + default_permission=None, + session_factory=None, + default_view_mapper=None, + exceptionresponse_view=default_exceptionresponse_view, + introspector=None): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial setup is performed against the registry. This is because the registry you pass in may @@ -287,6 +337,11 @@ class Configurator( registry = self.registry self._fix_registry() + + if introspector is not None: + # use nondefault introspector + self.introspector = introspector + self._set_settings(settings) self._register_response_adapters() @@ -416,8 +471,37 @@ class Configurator( _registry.registerSelfAdapter = registerSelfAdapter # API + + def _get_introspector(self): + introspector = getattr(self.registry, 'introspector', _marker) + if introspector is _marker: + introspector = Introspector() + self._set_introspector(introspector) + return introspector + + def _set_introspector(self, introspector): + self.registry.introspector = introspector + + def _del_introspector(self): + del self.registry.introspector + + introspector = property( + _get_introspector, _set_introspector, _del_introspector + ) + + @property + def action_info(self): + info = self.info # usually a ZCML action (ParserInfo) if self.info + if not info: + # Try to provide more accurate info for conflict reports + if self._ainfo: + info = self._ainfo[0] + else: + info = ActionInfo(None, 0, '', '') + return info - def action(self, discriminator, callable=None, args=(), kw=None, order=0): + def action(self, discriminator, callable=None, args=(), kw=None, order=0, + introspectables=(), **extra): """ Register an action which will be executed when :meth:`pyramid.config.Configurator.commit` is called (or executed immediately if ``autocommit`` is ``True``). @@ -430,40 +514,54 @@ class Configurator( given, but it can be ``None``, to indicate that the action never conflicts. It must be a hashable value. - The ``callable`` is a callable object which performs the action. It - is optional. ``args`` and ``kw`` are tuple and dict objects - respectively, which are passed to ``callable`` when this action is - executed. + The ``callable`` is a callable object which performs the task + associated with the action when the action is executed. It is + optional. + + ``args`` and ``kw`` are tuple and dict objects respectively, which + are passed to ``callable`` when this action is executed. Both are + optional. + + ``order`` is a grouping mechanism; an action with a lower order will + be executed before an action with a higher order (has no effect when + autocommit is ``True``). - ``order`` is a crude order control mechanism, only rarely used (has - no effect when autocommit is ``True``). + ``introspectables`` is a sequence of :term:`introspectable` objects + (or the empty sequence if no introspectable objects are associated + with this action). + + ``extra`` provides a facility for inserting extra keys and values + into an action dictionary. """ if kw is None: kw = {} autocommit = self.autocommit + action_info = self.action_info + introspector = self.introspector if autocommit: if callable is not None: callable(*args, **kw) + if introspector is not None: + for introspectable in introspectables: + introspectable.register(introspector, action_info) else: - info = self.info # usually a ZCML action if self.info has data - if not info: - # Try to provide more accurate info for conflict reports - if self._ainfo: - info = self._ainfo[0] - else: - info = '' - self.action_state.action( - discriminator, - callable, - args, - kw, - order, - info=info, - includepath=self.includepath, + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + order=order, + info=action_info, + includepath=self.includepath, + introspectables=introspectables, + ) ) + self.action_state.action(**action) def _get_action_state(self): registry = self.registry @@ -488,7 +586,7 @@ class Configurator( of this error will be information about the source of the conflict, usually including file names and line numbers of the cause of the configuration conflicts.""" - self.action_state.execute_actions() + self.action_state.execute_actions(introspector=self.introspector) self.action_state = ActionState() # old actions have been processed def include(self, callable, route_prefix=None): @@ -841,22 +939,27 @@ class ActionState(object): return True def action(self, discriminator, callable=None, args=(), kw=None, order=0, - includepath=(), info=''): + includepath=(), info=None, introspectables=(), **extra): """Add an action with the given discriminator, callable and arguments """ - # NB: note that the ordering and composition of the action tuple should - # not change without first ensuring that ``pyramid_zcml`` appends - # similarly-composed actions to our .actions variable (as silly as - # the composition and ordering is). if kw is None: kw = {} - action = (discriminator, callable, args, kw, includepath, info, order) - # remove trailing false items - while (len(action) > 2) and not action[-1]: - action = action[:-1] + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) + ) self.actions.append(action) - def execute_actions(self, clear=True): + def execute_actions(self, clear=True, introspector=None): """Execute the configuration actions This calls the action callables after resolving conflicts @@ -897,21 +1000,26 @@ class ActionState(object): in: oops - Note that actions executed before the error still have an effect: >>> output [('f', (1,), {}), ('f', (2,), {})] - """ + try: for action in resolveConflicts(self.actions): - _, callable, args, kw, _, info, _ = expand_action(*action) - if callable is None: - continue + callable = action['callable'] + args = action['args'] + kw = action['kw'] + info = action['info'] + # we use "get" below in case an action was added via a ZCML + # directive that did not know about introspectables + introspectables = action.get('introspectables', ()) + try: - callable(*args, **kw) + if callable is not None: + callable(*args, **kw) except (KeyboardInterrupt, SystemExit): # pragma: no cover raise except: @@ -922,6 +1030,11 @@ class ActionState(object): tb) finally: del t, v, tb + + if introspector is not None: + for introspectable in introspectables: + introspectable.register(introspector, info) + finally: if clear: del self.actions[:] @@ -931,120 +1044,95 @@ def resolveConflicts(actions): """Resolve conflicting actions Given an actions list, identify and try to resolve conflicting actions. - Actions conflict if they have the same non-null discriminator. + Actions conflict if they have the same non-None discriminator. Conflicting actions can be resolved if the include path of one of the actions is a prefix of the includepaths of the other conflicting actions and is unequal to the include paths in the other conflicting actions. - - Here are some examples to illustrate how this works: - - >>> from zope.configmachine.tests.directives import f - >>> from pprint import PrettyPrinter - >>> pprint=PrettyPrinter(width=60).pprint - >>> pprint(resolveConflicts([ - ... (None, f), - ... (1, f, (1,), {}, (), 'first'), - ... (1, f, (2,), {}, ('x',), 'second'), - ... (1, f, (3,), {}, ('y',), 'third'), - ... (4, f, (4,), {}, ('y',), 'should be last', 99999), - ... (3, f, (3,), {}, ('y',)), - ... (None, f, (5,), {}, ('y',)), - ... ])) - [(None, f), - (1, f, (1,), {}, (), 'first'), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - (4, f, (4,), {}, ('y',), 'should be last')] - - >>> try: - ... v = resolveConflicts([ - ... (None, f), - ... (1, f, (2,), {}, ('x',), 'eek'), - ... (1, f, (3,), {}, ('y',), 'ack'), - ... (4, f, (4,), {}, ('y',)), - ... (3, f, (3,), {}, ('y',)), - ... (None, f, (5,), {}, ('y',)), - ... ]) - ... except ConfigurationConflictError, v: - ... pass - >>> print v - Conflicting configuration actions - For: 1 - eek - ack - """ # organize actions by discriminators unique = {} output = [] - for i in range(len(actions)): - (discriminator, callable, args, kw, includepath, info, order - ) = expand_action(*(actions[i])) + for i, action in enumerate(actions): + if not isinstance(action, dict): + # old-style tuple action + action = expand_action(*action) + + # "order" is an integer grouping. Actions in a lower order will be + # executed before actions in a higher order. Within an order, + # actions are executed sequentially based on original action ordering + # ("i"). + order = action['order'] or 0 + discriminator = action['discriminator'] + + # "ainfo" is a tuple of (order, i, action) where "order" is a + # user-supplied grouping, "i" is an integer expressing the relative + # position of this action in the action list being resolved, and + # "action" is an action dictionary. The purpose of an ainfo is to + # associate an "order" and an "i" with a particular action; "order" + # and "i" exist for sorting purposes after conflict resolution. + ainfo = (order, i, action) - order = order or i if discriminator is None: - # The discriminator is None, so this directive can - # never conflict. We can add it directly to the - # configuration actions. - output.append( - (order, discriminator, callable, args, kw, includepath, info) - ) + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) continue - - a = unique.setdefault(discriminator, []) - a.append( - (includepath, order, callable, args, kw, info) - ) + L = unique.setdefault(discriminator, []) + L.append(ainfo) # Check for conflicts conflicts = {} - for discriminator, dups in unique.items(): - - # We need to sort the actions by the paths so that the shortest - # path with a given prefix comes first: - def allbutfunc(stupid): - # f me with a shovel, py3 cant cope with sorting when the - # callable function is in the list - return stupid[0:2] + stupid[3:] - dups.sort(key=allbutfunc) - (basepath, i, callable, args, kw, baseinfo) = dups[0] - output.append( - (i, discriminator, callable, args, kw, basepath, baseinfo) - ) - for includepath, i, callable, args, kw, info in dups[1:]: + + for discriminator, ainfos in unique.items(): + + # We use (includepath, order, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "order", then + # "i". + def bypath(ainfo): + path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] + return path, order, i + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] + output.append(ainfo) + _, _, action = ainfo + basepath, baseinfo, discriminator = (action['includepath'], + action['info'], + action['discriminator']) + + for _, _, action in rest: + includepath = action['includepath'] # Test whether path is a prefix of opath if (includepath[:len(basepath)] != basepath # not a prefix - or - (includepath == basepath) - ): - if discriminator not in conflicts: - conflicts[discriminator] = [baseinfo] - conflicts[discriminator].append(info) - + or includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) if conflicts: raise ConfigurationConflictError(conflicts) - # Now put the output back in the original order, and return it: - output.sort() - r = [] - for o in output: - action = o[1:] - while len(action) > 2 and not action[-1]: - action = action[:-1] - r.append(action) - - return r - -# this function is licensed under the ZPL (stolen from Zope) + # sort conflict-resolved actions by (order, i) and return them + return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))] + def expand_action(discriminator, callable=None, args=(), kw=None, - includepath=(), info='', order=0): + includepath=(), info=None, order=0, introspectables=()): if kw is None: kw = {} - return (discriminator, callable, args, kw, includepath, info, order) + return dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) global_registries = WeakOrderedSet() diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index f022e7f08..04571bec3 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -27,7 +27,13 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): self.registry.registerHandler(subscriber, iface) - self.action(None, register) + intr = self.introspectable('subscribers', + id(subscriber), + self.object_description(subscriber), + 'subscriber') + intr['subscriber'] = subscriber + intr['interfaces'] = iface + self.action(None, register, introspectables=(intr,)) return subscriber @action_method @@ -52,7 +58,15 @@ class AdaptersConfiguratorMixin(object): reg.registerSelfAdapter((type_or_iface,), IResponse) else: reg.registerAdapter(adapter, (type_or_iface,), IResponse) - self.action((IResponse, type_or_iface), register) + discriminator = (IResponse, type_or_iface) + intr = self.introspectable( + 'response adapters', + discriminator, + self.object_description(adapter), + 'response adapter') + intr['adapter'] = adapter + intr['type'] = type_or_iface + self.action(discriminator, register, introspectables=(intr,)) def _register_response_adapters(self): # cope with WebOb response objects that aren't decorated with IResponse diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 08cc6dc38..c93431987 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -236,7 +236,15 @@ class AssetsConfiguratorMixin(object): to_package = sys.modules[override_package] override(from_package, path, to_package, override_prefix) - self.action(None, register) + intr = self.introspectable( + 'asset overrides', + (package, override_package, path, override_prefix), + '%s -> %s' % (to_override, override_with), + 'asset override', + ) + intr['to_override'] = to_override + intr['override_with'] = override_with + self.action(None, register, introspectables=(intr,)) override_resource = override_asset # bw compat diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 0e59f9286..530b6cc28 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,9 +1,11 @@ from pyramid.config.util import action_method -from pyramid.interfaces import IDefaultRootFactory -from pyramid.interfaces import IRequestFactory -from pyramid.interfaces import IRootFactory -from pyramid.interfaces import ISessionFactory +from pyramid.interfaces import ( + IDefaultRootFactory, + IRequestFactory, + IRootFactory, + ISessionFactory, + ) from pyramid.traversal import DefaultRootFactory @@ -26,15 +28,21 @@ class FactoriesConfiguratorMixin(object): def register(): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - self.action(IRootFactory, register) + + intr = self.introspectable('root factories', + None, + self.object_description(factory), + 'root factory') + intr['factory'] = factory + self.action(IRootFactory, register, introspectables=(intr,)) _set_root_factory = set_root_factory # bw compat @action_method - def set_session_factory(self, session_factory): + def set_session_factory(self, factory): """ Configure the application with a :term:`session factory`. If this - method is called, the ``session_factory`` argument must be a session + method is called, the ``factory`` argument must be a session factory callable or a :term:`dotted Python name` to that factory. .. note:: @@ -43,10 +51,14 @@ class FactoriesConfiguratorMixin(object): :class:`pyramid.config.Configurator` constructor can be used to achieve the same purpose. """ - session_factory = self.maybe_dotted(session_factory) + factory = self.maybe_dotted(factory) def register(): - self.registry.registerUtility(session_factory, ISessionFactory) - self.action(ISessionFactory, register) + self.registry.registerUtility(factory, ISessionFactory) + intr = self.introspectable('session factory', None, + self.object_description(factory), + 'session factory') + intr['factory'] = factory + self.action(ISessionFactory, register, introspectables=(intr,)) @action_method def set_request_factory(self, factory): @@ -67,5 +79,9 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRequestFactory) - self.action(IRequestFactory, register) + intr = self.introspectable('request factory', None, + self.object_description(factory), + 'request factory') + intr['factory'] = factory + self.action(IRequestFactory, register, introspectables=(intr,)) diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 6eed99191..67a7e2018 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -3,9 +3,11 @@ import sys from translationstring import ChameleonTranslate -from pyramid.interfaces import IChameleonTranslate -from pyramid.interfaces import ILocaleNegotiator -from pyramid.interfaces import ITranslationDirectories +from pyramid.interfaces import ( + IChameleonTranslate, + ILocaleNegotiator, + ITranslationDirectories, + ) from pyramid.exceptions import ConfigurationError from pyramid.i18n import get_localizer @@ -38,12 +40,17 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - self.action(ILocaleNegotiator, register) + intr = self.introspectable('locale negotiator', None, + self.object_description(negotiator), + 'locale negotiator') + intr['negotiator'] = negotiator + self.action(ILocaleNegotiator, register, introspectables=(intr,)) def _set_locale_negotiator(self, negotiator): locale_negotiator = self.maybe_dotted(negotiator) self.registry.registerUtility(locale_negotiator, ILocaleNegotiator) + @action_method def add_translation_dirs(self, *specs): """ Add one or more :term:`translation directory` paths to the current configuration state. The ``specs`` argument is a @@ -69,8 +76,10 @@ class I18NConfiguratorMixin(object): in the order they're provided in the ``*specs`` list argument (items earlier in the list trump ones later in the list). """ - for spec in specs[::-1]: # reversed + directories = [] + introspectables = [] + for spec in specs[::-1]: # reversed package_name, filename = self._split_spec(spec) if package_name is None: # absolute filename directory = filename @@ -80,25 +89,35 @@ class I18NConfiguratorMixin(object): directory = os.path.join(package_path(package), filename) if not os.path.isdir(os.path.realpath(directory)): - raise ConfigurationError('"%s" is not a directory' % directory) + raise ConfigurationError('"%s" is not a directory' % + directory) + intr = self.introspectable('translation directories', directory, + spec, 'translation directory') + intr['directory'] = directory + intr['spec'] = spec + introspectables.append(intr) + directories.append(directory) - tdirs = self.registry.queryUtility(ITranslationDirectories) - if tdirs is None: - tdirs = [] - self.registry.registerUtility(tdirs, ITranslationDirectories) + def register(): + for directory in directories: - tdirs.insert(0, directory) - # XXX no action? + tdirs = self.registry.queryUtility(ITranslationDirectories) + if tdirs is None: + tdirs = [] + self.registry.registerUtility(tdirs, + ITranslationDirectories) - if specs: + tdirs.insert(0, directory) - # We actually only need an IChameleonTranslate function - # utility to be registered zero or one times. We register the - # same function once for each added translation directory, - # which does too much work, but has the same effect. + if directories: + # We actually only need an IChameleonTranslate function + # utility to be registered zero or one times. We register the + # same function once for each added translation directory, + # which does too much work, but has the same effect. + ctranslate = ChameleonTranslate(translator) + self.registry.registerUtility(ctranslate, IChameleonTranslate) - ctranslate = ChameleonTranslate(translator) - self.registry.registerUtility(ctranslate, IChameleonTranslate) + self.action(None, register, introspectables=introspectables) def translator(msg): request = get_current_request() diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index f70dea118..926511b7b 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -1,14 +1,19 @@ import warnings -from pyramid.interfaces import IRendererFactory -from pyramid.interfaces import IRendererGlobalsFactory -from pyramid.interfaces import PHASE1_CONFIG +from pyramid.interfaces import ( + IRendererFactory, + IRendererGlobalsFactory, + PHASE1_CONFIG, + ) from pyramid.config.util import action_method -from pyramid import renderers -from pyramid import chameleon_text -from pyramid import chameleon_zpt +from pyramid import ( + renderers, + chameleon_text, + chameleon_zpt, + ) + from pyramid.mako_templating import renderer_factory as mako_renderer_factory DEFAULT_RENDERERS = ( @@ -43,9 +48,16 @@ class RenderingConfiguratorMixin(object): name = '' def register(): self.registry.registerUtility(factory, IRendererFactory, name=name) + intr = self.introspectable('renderer factories', + name, + self.object_description(factory), + 'renderer factory') + intr['factory'] = factory + intr['name'] = name # we need to register renderers early (in phase 1) because they are # used during view configuration (which happens in phase 3) - self.action((IRendererFactory, name), register, order=PHASE1_CONFIG) + self.action((IRendererFactory, name), register, order=PHASE1_CONFIG, + introspectables=(intr,)) @action_method def set_renderer_globals_factory(self, factory, warn=True): @@ -63,7 +75,9 @@ class RenderingConfiguratorMixin(object): .. warning:: - This method is deprecated as of Pyramid 1.1. + This method is deprecated as of Pyramid 1.1. Use a BeforeRender + event subscriber as documented in the :ref:`hooks_chapter` chapter + instead. .. note:: @@ -83,4 +97,8 @@ class RenderingConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRendererGlobalsFactory) + intr = self.introspectable('renderer globals factory', None, + self.object_description(factory), + 'renderer globals factory') + intr['factory'] = factory self.action(IRendererGlobalsFactory, register) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 4008a2e08..ea39b6805 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -1,16 +1,21 @@ import warnings -from pyramid.interfaces import IRequest -from pyramid.interfaces import IRouteRequest -from pyramid.interfaces import IRoutesMapper -from pyramid.interfaces import PHASE2_CONFIG +from pyramid.interfaces import ( + IRequest, + IRouteRequest, + IRoutesMapper, + PHASE2_CONFIG, + ) from pyramid.exceptions import ConfigurationError from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper -from pyramid.config.util import action_method -from pyramid.config.util import make_predicates +from pyramid.config.util import ( + action_method, + make_predicates, + as_sorted_tuple, + ) class RoutesConfiguratorMixin(object): @action_method @@ -343,6 +348,9 @@ class RoutesConfiguratorMixin(object): """ # these are route predicates; if they do not match, the next route # in the routelist will be tried + if request_method is not None: + request_method = as_sorted_tuple(request_method) + ignored, predicates, ignored = make_predicates( xhr=xhr, request_method=request_method, @@ -365,6 +373,38 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() + introspectables = [] + + intr = self.introspectable('routes', + name, + '%s (pattern: %r)' % (name, pattern), + 'route') + intr['name'] = name + intr['pattern'] = pattern + intr['factory'] = factory + intr['xhr'] = xhr + intr['request_methods'] = request_method + intr['path_info'] = path_info + intr['request_param'] = request_param + intr['header'] = header + intr['accept'] = accept + intr['traverse'] = traverse + intr['custom_predicates'] = custom_predicates + intr['pregenerator'] = pregenerator + intr['static'] = static + intr['use_global_views'] = use_global_views + introspectables.append(intr) + + if factory: + factory_intr = self.introspectable('root factories', + name, + self.object_description(factory), + 'root factory') + factory_intr['factory'] = factory + factory_intr['route_name'] = name + factory_intr.relate('routes', name) + introspectables.append(factory_intr) + def register_route_request_iface(): request_iface = self.registry.queryUtility(IRouteRequest, name=name) if request_iface is None: @@ -377,9 +417,12 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): - return mapper.connect(name, pattern, factory, predicates=predicates, - pregenerator=pregenerator, static=static) - + route = mapper.connect( + name, pattern, factory, predicates=predicates, + pregenerator=pregenerator, static=static + ) + intr['object'] = route + return route # We have to connect routes in the order they were provided; # we can't use a phase to do that, because when the actions are @@ -389,7 +432,7 @@ class RoutesConfiguratorMixin(object): # But IRouteRequest interfaces must be registered before we begin to # process view registrations (in phase 3) self.action(('route', name), register_route_request_iface, - order=PHASE2_CONFIG) + order=PHASE2_CONFIG, introspectables=introspectables) # deprecated adding views from add_route; must come after # route registration for purposes of autocommit ordering diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 935e2fd95..a0ea173ba 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -1,8 +1,10 @@ -from pyramid.interfaces import IAuthorizationPolicy -from pyramid.interfaces import IAuthenticationPolicy -from pyramid.interfaces import IDefaultPermission -from pyramid.interfaces import PHASE1_CONFIG -from pyramid.interfaces import PHASE2_CONFIG +from pyramid.interfaces import ( + IAuthorizationPolicy, + IAuthenticationPolicy, + IDefaultPermission, + PHASE1_CONFIG, + PHASE2_CONFIG, + ) from pyramid.exceptions import ConfigurationError from pyramid.config.util import action_method @@ -29,8 +31,13 @@ class SecurityConfiguratorMixin(object): 'Cannot configure an authentication policy without ' 'also configuring an authorization policy ' '(use the set_authorization_policy method)') + intr = self.introspectable('authentication policy', None, + self.object_description(policy), + 'authentication policy') + intr['policy'] = policy # authentication policy used by view config (phase 3) - self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG) + self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG, + introspectables=(intr,)) def _set_authentication_policy(self, policy): policy = self.maybe_dotted(policy) @@ -60,6 +67,10 @@ class SecurityConfiguratorMixin(object): 'also configuring an authentication policy ' '(use the set_authorization_policy method)') + intr = self.introspectable('authorization policy', None, + self.object_description(policy), + 'authorization policy') + intr['policy'] = policy # authorization policy used by view config (phase 3) and # authentication policy (phase 2) self.action(IAuthorizationPolicy, register, order=PHASE1_CONFIG) @@ -108,9 +119,20 @@ class SecurityConfiguratorMixin(object): :class:`pyramid.config.Configurator` constructor can be used to achieve the same purpose. """ - # default permission used during view registration (phase 3) def register(): self.registry.registerUtility(permission, IDefaultPermission) - self.action(IDefaultPermission, register, order=PHASE1_CONFIG) + intr = self.introspectable('default permission', + None, + permission, + 'default permission') + intr['value'] = permission + perm_intr = self.introspectable('permissions', + permission, + permission, + 'permission') + perm_intr['value'] = permission + # default permission used during view registration (phase 3) + self.action(IDefaultPermission, register, order=PHASE1_CONFIG, + introspectables=(intr, perm_intr,)) diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index 36729acdf..3cdc1aa24 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -1,9 +1,11 @@ from zope.interface import Interface -from pyramid.interfaces import ITraverser -from pyramid.interfaces import IAuthorizationPolicy -from pyramid.interfaces import IAuthenticationPolicy -from pyramid.interfaces import IRendererFactory +from pyramid.interfaces import ( + ITraverser, + IAuthorizationPolicy, + IAuthenticationPolicy, + IRendererFactory, + ) from pyramid.renderers import RendererHelper from pyramid.traversal import traversal_path_info diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 048309451..1a83f0de9 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -2,12 +2,19 @@ from zope.interface import implementer from pyramid.interfaces import ITweens -from pyramid.compat import string_types -from pyramid.compat import is_nonstr_iter -from pyramid.compat import string_types +from pyramid.compat import ( + string_types, + is_nonstr_iter, + ) + from pyramid.exceptions import ConfigurationError -from pyramid.tweens import excview_tween_factory -from pyramid.tweens import MAIN, INGRESS, EXCVIEW + +from pyramid.tweens import ( + excview_tween_factory, + MAIN, + INGRESS, + EXCVIEW, + ) from pyramid.config.util import action_method @@ -131,11 +138,22 @@ class TweensConfiguratorMixin(object): raise ConfigurationError('%s cannot be under MAIN' % name) registry = self.registry + introspectables = [] tweens = registry.queryUtility(ITweens) if tweens is None: tweens = Tweens() registry.registerUtility(tweens, ITweens) + ex_intr = self.introspectable('tweens', + ('tween', EXCVIEW, False), + EXCVIEW, + 'implicit tween') + ex_intr['name'] = EXCVIEW + ex_intr['factory'] = excview_tween_factory + ex_intr['type'] = 'implicit' + ex_intr['under'] = None + ex_intr['over'] = MAIN + introspectables.append(ex_intr) tweens.add_implicit(EXCVIEW, excview_tween_factory, over=MAIN) def register(): @@ -144,7 +162,20 @@ class TweensConfiguratorMixin(object): else: tweens.add_implicit(name, tween_factory, under=under, over=over) - self.action(('tween', name, explicit), register) + discriminator = ('tween', name, explicit) + tween_type = explicit and 'explicit' or 'implicit' + + intr = self.introspectable('tweens', + discriminator, + name, + '%s tween' % tween_type) + intr['name'] = name + intr['factory'] = tween_factory + intr['type'] = tween_type + intr['under'] = under + intr['over'] = over + introspectables.append(intr) + self.action(discriminator, register, introspectables=introspectables) class CyclicDependencyError(Exception): def __init__(self, cycles): @@ -184,7 +215,7 @@ class Tweens(object): self.order += [(u, name) for u in under] self.req_under.add(name) if over is not None: - if not is_nonstr_iter(over): #hasattr(over, '__iter__'): + if not is_nonstr_iter(over): over = (over,) self.order += [(name, o) for o in over] self.req_over.add(name) diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 0336b103d..b0e873de3 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,18 +1,41 @@ import re import traceback -from pyramid.compat import string_types -from pyramid.compat import bytes_ -from pyramid.compat import is_nonstr_iter +from zope.interface import implementer + +from pyramid.interfaces import IActionInfo + +from pyramid.compat import ( + string_types, + bytes_, + is_nonstr_iter, + ) + from pyramid.exceptions import ConfigurationError -from pyramid.traversal import find_interface -from pyramid.traversal import traversal_path_info + +from pyramid.traversal import ( + find_interface, + traversal_path_info, + ) from hashlib import md5 MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + self.file = file + self.line = line + self.function = function + self.src = src + + def __str__(self): + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) + def action_method(wrapped): """ Wrapper to provide the right conflict info report data when a method that calls Configurator.action calls another that does the same""" @@ -20,12 +43,17 @@ def action_method(wrapped): if self._ainfo is None: self._ainfo = [] info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 2) + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) if info is None: try: f = traceback.extract_stack(limit=3) - info = f[-2] + info = ActionInfo(*f[-backframes]) except: # pragma: no cover - info = '' + info = ActionInfo(None, 0, '', '') self._ainfo.append(info) try: result = wrapped(self, *arg, **kw) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 179d4065c..5efe1f2bb 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,48 +1,68 @@ import inspect +import operator +from functools import wraps + +from zope.interface import ( + Interface, + classProvides, + implementedBy, + implementer, + ) -from zope.interface import Interface -from zope.interface import classProvides -from zope.interface import implementedBy -from zope.interface import implementer from zope.interface.interfaces import IInterface -from pyramid.interfaces import IAuthenticationPolicy -from pyramid.interfaces import IAuthorizationPolicy -from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IDefaultPermission -from pyramid.interfaces import IException -from pyramid.interfaces import IExceptionViewClassifier -from pyramid.interfaces import IMultiView -from pyramid.interfaces import IRendererFactory -from pyramid.interfaces import IRequest -from pyramid.interfaces import IResponse -from pyramid.interfaces import IRouteRequest -from pyramid.interfaces import ISecuredView -from pyramid.interfaces import IStaticURLInfo -from pyramid.interfaces import IView -from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import IViewMapper -from pyramid.interfaces import IViewMapperFactory -from pyramid.interfaces import PHASE1_CONFIG +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + IDebugLogger, + IDefaultPermission, + IException, + IExceptionViewClassifier, + IMultiView, + IRendererFactory, + IRequest, + IResponse, + IRouteRequest, + ISecuredView, + IStaticURLInfo, + IView, + IViewClassifier, + IViewMapper, + IViewMapperFactory, + PHASE1_CONFIG, + ) from pyramid import renderers -from pyramid.compat import string_types -from pyramid.compat import urlparse -from pyramid.compat import im_func -from pyramid.exceptions import ConfigurationError -from pyramid.exceptions import PredicateMismatch -from pyramid.httpexceptions import HTTPForbidden -from pyramid.httpexceptions import HTTPNotFound + +from pyramid.compat import ( + string_types, + urlparse, + im_func, + url_quote, + ) + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, + ) + +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPNotFound, + ) + from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry from pyramid.view import render_view_to_response -from pyramid.config.util import DEFAULT_PHASH -from pyramid.config.util import MAX_ORDER -from pyramid.config.util import action_method -from pyramid.config.util import as_sorted_tuple -from pyramid.config.util import make_predicates +from pyramid.config.util import ( + DEFAULT_PHASH, + MAX_ORDER, + action_method, + as_sorted_tuple, + make_predicates, + ) urljoin = urlparse.urljoin url_parse = urlparse.urlparse @@ -481,7 +501,7 @@ class MultiView(object): if accept is None or '*' in accept: self.views.append((order, view, phash)) - self.views.sort() + self.views.sort(key=operator.itemgetter(0)) else: subset = self.media_views.setdefault(accept, []) subset.append((order, view, phash)) @@ -532,7 +552,22 @@ class MultiView(object): continue raise PredicateMismatch(self.name) +def viewdefaults(wrapped): + def wrapper(*arg, **kw): + defaults = {} + if len(arg) > 1: + view = arg[1] + else: + view = kw.get('view') + if inspect.isclass(view): + defaults = getattr(view, '__view_defaults__', {}).copy() + defaults.update(kw) + defaults['_backframes'] = 3 # for action_method + return wrapped(*arg, **defaults) + return wraps(wrapped)(wrapper) + class ViewsConfiguratorMixin(object): + @viewdefaults @action_method def add_view(self, view=None, name="", for_=None, permission=None, request_type=None, route_name=None, request_method=None, @@ -922,6 +957,42 @@ class ViewsConfiguratorMixin(object): name=renderer, package=self.package, registry = self.registry) + introspectables = [] + discriminator = [ + 'view', context, name, request_type, IView, containment, + request_param, request_method, route_name, attr, + xhr, accept, header, path_info, match_param] + discriminator.extend(sorted([hash(x) for x in custom_predicates])) + discriminator = tuple(discriminator) + if inspect.isclass(view) and attr: + view_desc = 'method %r of %s' % ( + attr, self.object_description(view)) + else: + view_desc = self.object_description(view) + view_intr = self.introspectable('views', + discriminator, + view_desc, + 'view') + view_intr.update( + dict(name=name, + context=context, + containment=containment, + request_param=request_param, + request_methods=request_method, + route_name=route_name, + attr=attr, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + match_param=match_param, + callable=view, + mapper=mapper, + decorator=decorator, + ) + ) + introspectables.append(view_intr) + def register(permission=permission, renderer=renderer): request_iface = IRequest if route_name is not None: @@ -963,6 +1034,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, http_cache=http_cache) derived_view = deriver(view) + view_intr['derived_callable'] = derived_view registered = self.registry.adapters.registered @@ -1060,13 +1132,33 @@ class ViewsConfiguratorMixin(object): (IExceptionViewClassifier, request_iface, context), IMultiView, name=name) - discriminator = [ - 'view', context, name, request_type, IView, containment, - request_param, request_method, route_name, attr, - xhr, accept, header, path_info, match_param] - discriminator.extend(sorted([hash(x) for x in custom_predicates])) - discriminator = tuple(discriminator) - self.action(discriminator, register) + if mapper: + mapper_intr = self.introspectable('view mappers', + discriminator, + 'view mapper for %s' % view_desc, + 'view mapper') + mapper_intr['mapper'] = mapper + mapper_intr.relate('views', discriminator) + introspectables.append(mapper_intr) + if route_name: + view_intr.relate('routes', route_name) # see add_route + if renderer is not None and renderer.name and '.' in renderer.name: + # it's a template + tmpl_intr = self.introspectable('templates', discriminator, + renderer.name, 'template') + tmpl_intr.relate('views', discriminator) + tmpl_intr['name'] = renderer.name + tmpl_intr['type'] = renderer.type + tmpl_intr['renderer'] = renderer + tmpl_intr.relate('renderer factories', renderer.type) + introspectables.append(tmpl_intr) + if permission is not None: + perm_intr = self.introspectable('permissions', permission, + permission, 'permission') + perm_intr['value'] = permission + perm_intr.relate('views', discriminator) + introspectables.append(perm_intr) + self.action(discriminator, register, introspectables=introspectables) def derive_view(self, view, attr=None, renderer=None): """ @@ -1293,7 +1385,13 @@ class ViewsConfiguratorMixin(object): self.registry.registerUtility(mapper, IViewMapperFactory) # IViewMapperFactory is looked up as the result of view config # in phase 3 - self.action(IViewMapperFactory, register, order=PHASE1_CONFIG) + intr = self.introspectable('view mappers', + IViewMapperFactory, + self.object_description(mapper), + 'default view mapper') + intr['mapper'] = mapper + self.action(IViewMapperFactory, register, order=PHASE1_CONFIG, + introspectables=(intr,)) @action_method def add_static_view(self, name, path, **kw): @@ -1428,7 +1526,7 @@ class StaticURLInfo(object): registry = get_current_registry() for (url, spec, route_name) in self._get_registrations(registry): if path.startswith(spec): - subpath = path[len(spec):] + subpath = url_quote(path[len(spec):]) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -1522,6 +1620,13 @@ class StaticURLInfo(object): # url, spec, route_name registrations.append((url, spec, route_name)) - config.action(None, callable=register) + intr = config.introspectable('static views', + name, + 'static view for %r' % name, + 'static view') + intr['name'] = name + intr['spec'] = spec + + config.action(None, callable=register, introspectables=(intr,)) diff --git a/pyramid/encode.py b/pyramid/encode.py index a259d1414..65bc95032 100644 --- a/pyramid/encode.py +++ b/pyramid/encode.py @@ -1,8 +1,10 @@ -from pyramid.compat import text_type -from pyramid.compat import binary_type -from pyramid.compat import is_nonstr_iter -from pyramid.compat import url_quote as _url_quote -from pyramid.compat import url_quote_plus as quote_plus # bw compat api (dnr) +from pyramid.compat import ( + text_type, + binary_type, + is_nonstr_iter, + url_quote as _url_quote, + url_quote_plus as quote_plus, # bw compat api (dnr) + ) def url_quote(s, safe=''): # bw compat api return _url_quote(s, safe=safe) diff --git a/pyramid/events.py b/pyramid/events.py index 5ef60ea89..9cb8b31ad 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -2,11 +2,13 @@ import venusian from zope.interface import implementer -from pyramid.interfaces import IContextFound -from pyramid.interfaces import INewRequest -from pyramid.interfaces import INewResponse -from pyramid.interfaces import IApplicationCreated -from pyramid.interfaces import IBeforeRender +from pyramid.interfaces import ( + IContextFound, + INewRequest, + INewResponse, + IApplicationCreated, + IBeforeRender, + ) class subscriber(object): """ Decorator activated via a :term:`scan` which treats the @@ -167,7 +169,7 @@ WSGIApplicationCreatedEvent = ApplicationCreated # b/c (as of 1.0) @implementer(IBeforeRender) class BeforeRender(dict): """ - Subscribers to this event may introspect the and modify the set of + Subscribers to this event may introspect and modify the set of :term:`renderer globals` before they are passed to a :term:`renderer`. This event object iself has a dictionary-like interface that can be used for this purpose. For example:: @@ -185,20 +187,22 @@ class BeforeRender(dict): :class:`pyramid.config.Configurator.set_renderer_globals_factory`, if any, has injected its own keys into the renderer globals dictionary). - If a subscriber adds a key via ``__setitem__`` or that already exists in - the renderer globals dictionary, it will overwrite an older value that is - already in the globals dictionary. This can be problematic because event - subscribers to the BeforeRender event do not possess any relative - ordering. For maximum interoperability with other third-party - subscribers, if you write an event subscriber meant to be used as a - BeforeRender subscriber, your subscriber code will need to (using - ``.get`` or ``__contains__`` of the event object) ensure no value already - exists in the renderer globals dictionary before setting an overriding - value. + If a subscriber adds a key via ``__setitem__`` that already exists in + the renderer globals dictionary, it will overwrite the older value there. + This can be problematic because event subscribers to the BeforeRender + event do not possess any relative ordering. For maximum interoperability + with other third-party subscribers, if you write an event subscriber meant + to be used as a BeforeRender subscriber, your subscriber code will need to + ensure no value already exists in the renderer globals dictionary before + setting an overriding value (which can be done using ``.get`` or + ``__contains__`` of the event object). The event has an additional attribute named ``rendering_val``. This is the (non-system) value returned by a view or passed to ``render*`` as ``value``. This feature is new in Pyramid 1.2. + + For a description of the values present in the renderer globals dictionary, + see :ref:`renderer_system_values`. See also :class:`pyramid.interfaces.IBeforeRender`. """ diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index ff598fe2d..04b6e20b7 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,5 +1,7 @@ -from pyramid.httpexceptions import HTTPNotFound -from pyramid.httpexceptions import HTTPForbidden +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPForbidden, + ) NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 4dbca7021..c82d6900c 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -128,12 +128,15 @@ from zope.interface import implementer from webob import html_escape as _html_escape +from pyramid.compat import ( + class_types, + text_type, + binary_type, + text_, + ) + from pyramid.interfaces import IExceptionResponse from pyramid.response import Response -from pyramid.compat import class_types -from pyramid.compat import text_type -from pyramid.compat import binary_type -from pyramid.compat import text_ def _no_escape(value): if value is None: @@ -434,6 +437,8 @@ ${html_comment}''') def __init__(self, location='', detail=None, headers=None, comment=None, body_template=None, **kw): + if location is None: + raise ValueError("HTTP redirects need a location to redirect to.") super(_HTTPMove, self).__init__( detail=detail, headers=headers, comment=comment, body_template=body_template, location=location, **kw) diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 889227130..b4bc0eaa7 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -1,18 +1,23 @@ import gettext import os -from translationstring import Translator -from translationstring import Pluralizer -from translationstring import TranslationString # API -from translationstring import TranslationStringFactory # API +from translationstring import ( + Translator, + Pluralizer, + TranslationString, # API + TranslationStringFactory, # API + ) TranslationString = TranslationString # PyFlakes TranslationStringFactory = TranslationStringFactory # PyFlakes from pyramid.compat import PY3 -from pyramid.interfaces import ILocalizer -from pyramid.interfaces import ITranslationDirectories -from pyramid.interfaces import ILocaleNegotiator + +from pyramid.interfaces import ( + ILocalizer, + ITranslationDirectories, + ILocaleNegotiator, + ) from pyramid.threadlocal import get_current_registry diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index f08bd5fbb..6762d788d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1,5 +1,7 @@ -from zope.interface import Attribute -from zope.interface import Interface +from zope.interface import ( + Attribute, + Interface, + ) from pyramid.compat import PY3 @@ -344,7 +346,7 @@ class IDict(Interface): class IBeforeRender(IDict): """ - Subscribers to this event may introspect the and modify the set of + Subscribers to this event may introspect and modify the set of :term:`renderer globals` before they are passed to a :term:`renderer`. The event object itself provides a dictionary-like interface for adding and removing :term:`renderer globals`. The keys and values of the @@ -857,9 +859,202 @@ class IRendererInfo(Interface): 'to the current application') +class IIntrospector(Interface): + def get(category_name, discriminator, default=None): + """ Get the IIntrospectable related to the category_name and the + discriminator (or discriminator hash) ``discriminator``. If it does + not exist in the introspector, return the value of ``default`` """ + + def get_category(category_name, default=None, sort_key=None): + """ Get a sequence of dictionaries in the form + ``[{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...]`` where each introspectable is part of the + category associated with ``category_name`` . + + If the category named ``category_name`` does not exist in the + introspector the value passed as ``default`` will be returned. + + If ``sort_key`` is ``None``, the sequence will be returned in the + order the introspectables were added to the introspector. Otherwise, + sort_key should be a function that accepts an IIntrospectable and + returns a value from it (ala the ``key`` function of Python's + ``sorted`` callable).""" + + def categories(): + """ Return a sorted sequence of category names known by + this introspector """ + + def categorized(sort_key=None): + """ Get a sequence of tuples in the form ``[(category_name, + [{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...])]`` representing all known + introspectables. If ``sort_key`` is ``None``, each introspectables + sequence will be returned in the order the introspectables were added + to the introspector. Otherwise, sort_key should be a function that + accepts an IIntrospectable and returns a value from it (ala the + ``key`` function of Python's ``sorted`` callable).""" + + def remove(category_name, discriminator): + """ Remove the IIntrospectable related to ``category_name`` and + ``discriminator`` from the introspector, and fix up any relations + that the introspectable participates in. This method will not raise + an error if an introspectable related to the category name and + discriminator does not exist.""" + + def related(intr): + """ Return a sequence of IIntrospectables related to the + IIntrospectable ``intr``. Return the empty sequence if no relations + for exist.""" + + def add(intr): + """ Add the IIntrospectable ``intr`` (use instead of + :meth:`pyramid.interfaces.IIntrospector.add` when you have a custom + IIntrospectable). Replaces any existing introspectable registered + using the same category/discriminator. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register`""" + + def relate(*pairs): + """ Given any number of of ``(category_name, discriminator)`` pairs + passed as positional arguments, relate the associated introspectables + to each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair has already been associated with another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + def unrelate(*pairs): + """ Given any number of of ``(category_name, discriminator)`` pairs + passed as positional arguments, unrelate the associated introspectables + from each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair is not already related to another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + +class IIntrospectable(Interface): + """ An introspectable object used for configuration introspection. In + addition to the methods below, objects which implement this interface + must also implement all the methods of Python's + ``collections.MutableMapping`` (the "dictionary interface"), and must be + hashable.""" + + title = Attribute('Text title describing this introspectable') + type_name = Attribute('Text type name describing this introspectable') + order = Attribute('integer order in which registered with introspector ' + '(managed by introspector, usually)') + category_name = Attribute('introspection category name') + discriminator = Attribute('introspectable discriminator (within category) ' + '(must be hashable)') + discriminator_hash = Attribute('an integer hash of the discriminator') + action_info = Attribute('An IActionInfo object representing the caller ' + 'that invoked the creation of this introspectable ' + '(usually a sentinel until updated during ' + 'self.register)') + + def relate(category_name, discriminator): + """ Indicate an intent to relate this IIntrospectable with another + IIntrospectable (the one associated with the ``category_name`` and + ``discriminator``) during action execution. + """ + + def unrelate(category_name, discriminator): + """ Indicate an intent to break the relationship between this + IIntrospectable with another IIntrospectable (the one associated with + the ``category_name`` and ``discriminator``) during action execution. + """ + + def register(introspector, action_info): + """ Register this IIntrospectable with an introspector. This method + is invoked during action execution. Adds the introspectable and its + relations to the introspector. ``introspector`` should be an object + implementing IIntrospector. ``action_info`` should be a object + implementing the interface :class:`pyramid.interfaces.IActionInfo` + representing the call that registered this introspectable. + Pseudocode for an implementation of this method: + + .. code-block:: python + + def register(self, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for methodname, category_name, discriminator in self._relations: + method = getattr(introspector, methodname) + method((i.category_name, i.discriminator), + (category_name, discriminator)) + """ + + def __hash__(): + + """ Introspectables must be hashable. The typical implementation of + an introsepectable's __hash__ is:: + + return hash((self.category_name,) + (self.discriminator,)) + """ + +class IActionInfo(Interface): + """ Class which provides code introspection capability associated with an + action. The ParserInfo class used by ZCML implements the same interface.""" + file = Attribute( + 'Filename of action-invoking code as a string') + line = Attribute( + 'Starting line number in file (as an integer) of action-invoking code.' + 'This will be ``None`` if the value could not be determined.') + + def __str__(): + """ Return a representation of the action information (including + source code from file, if possible) """ + +class IAssetDescriptor(Interface): + """ + Describes an :term:`asset`. + """ + + def absspec(): + """ + Returns the absolute asset specification for this asset + (e.g. ``mypackage:templates/foo.pt``). + """ + + def abspath(): + """ + Returns an absolute path in the filesystem to the asset. + """ + + def stream(): + """ + Returns an input stream for reading asset contents. Raises an + exception if the asset is a directory or does not exist. + """ + + def isdir(): + """ + Returns True if the asset is a directory, otherwise returns False. + """ + + def listdir(): + """ + Returns iterable of filenames of directory contents. Raises an + exception if asset is not a directory. + """ + + def exists(): + """ + Returns True if asset exists, otherwise returns False. + """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. PHASE1_CONFIG = -20 PHASE2_CONFIG = -10 + diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 29be339f2..761695220 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -2,13 +2,21 @@ import os import sys import threading -from zope.interface import implementer -from zope.interface import Interface +from zope.interface import ( + implementer, + Interface, + ) + +from pyramid.asset import ( + resolve_asset_spec, + abspath_from_asset_spec, + ) + +from pyramid.compat import ( + is_nonstr_iter, + reraise, + ) -from pyramid.asset import resolve_asset_spec -from pyramid.asset import abspath_from_asset_spec -from pyramid.compat import is_nonstr_iter -from pyramid.exceptions import ConfigurationError from pyramid.interfaces import ITemplateRenderer from pyramid.settings import asbool from pyramid.util import DottedNameResolver @@ -64,7 +72,7 @@ def renderer_factory(info): lookup = registry.queryUtility(IMakoLookup) if lookup is None: reload_templates = settings.get('reload_templates', False) - directories = settings.get('mako.directories', None) + directories = settings.get('mako.directories', []) module_directory = settings.get('mako.module_directory', None) input_encoding = settings.get('mako.input_encoding', 'utf-8') error_handler = settings.get('mako.error_handler', None) @@ -72,9 +80,6 @@ def renderer_factory(info): imports = settings.get('mako.imports', None) strict_undefined = settings.get('mako.strict_undefined', 'false') preprocessor = settings.get('mako.preprocessor', None) - if directories is None: - raise ConfigurationError( - 'Mako template used without a ``mako.directories`` setting') if not is_nonstr_iter(directories): directories = list(filter(None, directories.splitlines())) directories = [ abspath_from_asset_spec(d) for d in directories ] @@ -154,7 +159,7 @@ class MakoLookupTemplateRenderer(object): error=exc_info[1], traceback=exc_info[2] ) - raise MakoRenderingException(errtext) + reraise(MakoRenderingException(errtext), None, exc_info[2]) finally: del exc_info diff --git a/pyramid/paster.py b/pyramid/paster.py index 3fec6c556..cee437ec4 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -1,11 +1,17 @@ import os import zope.deprecation -from paste.deploy import loadapp -from pyramid.scripting import prepare +from paste.deploy import ( + loadapp, + appconfig, + ) +from pyramid.compat import configparser +from logging.config import fileConfig +from pyramid.scripting import prepare from pyramid.scaffolds import PyramidTemplate # bw compat + PyramidTemplate = PyramidTemplate # pyflakes zope.deprecation.deprecated( @@ -20,16 +26,52 @@ def get_app(config_uri, name=None, loadapp=loadapp): 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() + app = loadapp(config_name, name=section, relative_to=here_dir) + return app + +def get_appsettings(config_uri, name=None, appconfig=appconfig): + """ Return a dictionary representing the key/value pairs in an ``app` + section within the file represented by ``config_uri``. + + 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) + +def setup_logging(config_uri, fileConfig=fileConfig, + configparser=configparser): + """ + Set up logging via the logging module's fileConfig function with the + filename specified via ``config_uri`` (a string in the form + ``filename#sectionname``). + + ConfigParser defaults are specified for the special ``__file__`` + and ``here`` variables, similar to PasteDeploy config loading. + """ + path, _ = _getpathsec(config_uri, None) + parser = configparser.ConfigParser() + parser.read([path]) + if parser.has_section('loggers'): + config_file = os.path.abspath(path) + return fileConfig( + config_file, + dict(__file__=config_file, here=os.path.dirname(config_file)) + ) + +def _getpathsec(config_uri, name): if '#' in config_uri: path, section = config_uri.split('#', 1) else: path, section = config_uri, 'main' if name: section = name - config_name = 'config:%s' % path - here_dir = os.getcwd() - app = loadapp(config_name, name=section, relative_to=here_dir) - return app + return path, section def bootstrap(config_uri, request=None): """ Load a WSGI application from the PasteDeploy config file specified diff --git a/pyramid/path.py b/pyramid/path.py index 9c7be4c57..8a8898174 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -3,6 +3,12 @@ import pkg_resources import sys import imp +from zope.interface import implementer + +from pyramid.interfaces import IAssetDescriptor + +from pyramid.compat import string_types + ignore_types = [ imp.C_EXTENSION, imp.C_BUILTIN ] init_names = [ '__init__%s' % x[0] for x in imp.get_suffixes() if x[0] and x[2] not in ignore_types ] @@ -25,7 +31,7 @@ def package_name(pkg_or_module): package name of the package in which the module lives. If this function is passed a package, return the dotted Python package name of the package itself.""" - if pkg_or_module is None: + if pkg_or_module is None or pkg_or_module.__name__ == '__main__': return '__main__' pkg_filename = pkg_or_module.__file__ pkg_name = pkg_or_module.__name__ @@ -68,3 +74,354 @@ def package_path(package): pass return prefix +class _CALLER_PACKAGE(object): + def __repr__(self): # pragma: no cover (for docs) + return 'pyramid.path.CALLER_PACKAGE' + +CALLER_PACKAGE = _CALLER_PACKAGE() + +class Resolver(object): + def __init__(self, package=CALLER_PACKAGE): + if package in (None, CALLER_PACKAGE): + self.package = package + else: + if isinstance(package, string_types): + try: + __import__(package) + except ImportError: + raise ValueError( + 'The dotted name %r cannot be imported' % (package,) + ) + package = sys.modules[package] + self.package = package_of(package) + + def get_package_name(self): + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = self.package.__name__ + return package_name + + def get_package(self): + if self.package is CALLER_PACKAGE: + package = caller_package() + else: + package = self.package + return package + + +class AssetResolver(Resolver): + """ A class used to resolve an :term:`asset specification` to an + :term:`asset descriptor`. + + .. note:: This API is new as of Pyramid 1.3. + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + + The ``package`` is used when a relative asset specification is supplied + to the :meth:`~pyramid.path.AssetResolver.resolve` method. An asset + specification without a colon in it is treated as relative. + + If the value ``None`` is supplied as the ``package``, the resolver will + only be able to resolve fully qualified (not relative) asset + specifications. Any attempt to resolve a relative asset specification + when the ``package`` is ``None`` will result in an :exc:`ValueError` + exception. + + If the value :attr:`pyramid.path.CALLER_PACKAGE` is supplied as the + ``package``, the resolver will treat relative asset specifications as + relative to the caller of the :meth:`~pyramid.path.AssetResolver.resolve` + method. + + If a *module* or *module name* (as opposed to a package or package name) + is supplied as ``package``, its containing package is computed and this + package used to derive the package name (all names are resolved relative + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``template.pt`` is supplied to the + :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting absolute + asset spec would be ``xml.minidom:template.pt``, because + ``xml.dom.expatbuilder`` is a module object, not a package object. + + If a *package* or *package name* (as opposed to a module or module name) + is supplied as ``package``, this package will be used to compute relative + asset specifications. For example, if the ``package`` argument to this + type was passed the string ``xml.dom``, and ``template.pt`` is supplied + to the :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting + absolute asset spec would be ``xml.minidom:template.pt``. + """ + def resolve(self, spec): + """ + Resolve the asset spec named as ``spec`` to an object that has the + attributes and methods described in + `pyramid.interfaces.IAssetDescriptor`. + + If ``spec`` is an absolute filename + (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset + spec (e.g. ``myproject:templates.foo.pt``), an asset descriptor is + returned without taking into account the ``package`` passed to this + class' constructor. + + If ``spec`` is a *relative* asset specification (an asset + specification without a ``:`` in it, e.g. ``templates/foo.pt``), the + ``package`` argument of the constructor is used as the the package + portion of the asset spec. For example: + + .. code-block:: python + + a = AssetResolver('myproject') + resolver = a.resolve('templates/foo.pt') + print resolver.abspath() + # -> /path/to/myproject/templates/foo.pt + + If the AssetResolver is constructed without a ``package`` argument of + ``None``, and a relative asset specification is passed to + ``resolve``, an :exc:`ValueError` exception is raised. + """ + if os.path.isabs(spec): + return FSAssetDescriptor(spec) + path = spec + if ':' in path: + package_name, path = spec.split(':', 1) + else: + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = getattr(self.package, '__name__', None) + if package_name is None: + raise ValueError( + 'relative spec %r irresolveable without package' % (spec,) + ) + return PkgResourcesAssetDescriptor(package_name, path) + +class DottedNameResolver(Resolver): + """ A class used to resolve a :term:`dotted Python name` to a package or + module object. + + .. note:: This API is new as of Pyramid 1.3. + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + + The ``package`` is used when a relative dotted name is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method. A dotted name + which has a ``.`` (dot) or ``:`` (colon) as its first character is + treated as relative. + + If the value ``None`` is supplied as the ``package``, the resolver will + only be able to resolve fully qualified (not relative) names. Any + attempt to resolve a relative name when the ``package`` is ``None`` will + result in an :exc:`ValueError` exception. + + If the value :attr:`pyramid.path.CALLER_PACKAGE` is supplied as the + ``package``, the resolver will treat relative dotted names as relative to + the caller of the :meth:`~pyramid.path.DottedNameResolver.resolve` + method. + + If a *module* or *module name* (as opposed to a package or package name) + is supplied as ``package``, its containing package is computed and this + package used to derive the package name (all names are resolved relative + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``.mindom`` is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting + import would be for ``xml.minidom``, because ``xml.dom.expatbuilder`` is + a module object, not a package object. + + If a *package* or *package name* (as opposed to a module or module name) + is supplied as ``package``, this package will be used to relative compute + dotted names. For example, if the ``package`` argument to this type was + passed the string ``xml.dom``, and ``.minidom`` is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting + import would be for ``xml.minidom``. + """ + def resolve(self, dotted): + """ + This method resolves a dotted name reference to a global Python + object (an object which can be imported) to the object itself. + + Two dotted name styles are supported: + + - ``pkg_resources``-style dotted names where non-module attributes + of a package are separated from the rest of the path using a ``:`` + e.g. ``package.module:attr``. + + - ``zope.dottedname``-style dotted names where non-module + attributes of a package are separated from the rest of the path + using a ``.`` e.g. ``package.module.attr``. + + These styles can be used interchangeably. If the supplied name + contains a ``:`` (colon), the ``pkg_resources`` resolution + mechanism will be chosen, otherwise the ``zope.dottedname`` + resolution mechanism will be chosen. + + If the ``dotted`` argument passed to this method is not a string, a + :exc:`ValueError` will be raised. + + When a dotted name cannot be resolved, a :exc:`ValueError` error is + raised. + + Example: + + .. code-block:: python + + r = DottedNameResolver() + v = r.resolve('xml') # v is the xml module + + """ + if not isinstance(dotted, string_types): + raise ValueError('%r is not a string' % (dotted,)) + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) + + def maybe_resolve(self, dotted): + """ + This method behaves just like + :meth:`~pyramid.path.DottedNameResolver.resolve`, except if the + ``dotted`` value passed is not a string, it is simply returned. For + example: + + .. code-block:: python + + import xml + r = DottedNameResolver() + v = r.maybe_resolve(xml) + # v is the xml module; no exception raised + """ + if isinstance(dotted, string_types): + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) + return dotted + + def _resolve(self, dotted, package): + if ':' in dotted: + return self._pkg_resources_style(dotted, package) + else: + return self._zope_dottedname_style(dotted, package) + + def _pkg_resources_style(self, value, package): + """ package.module:attr style """ + if value.startswith('.') or value.startswith(':'): + if not package: + raise ValueError( + 'relative name %r irresolveable without package' % (value,) + ) + if value in ['.', ':']: + value = package.__name__ + else: + value = package.__name__ + value + return pkg_resources.EntryPoint.parse( + 'x=%s' % value).load(False) + + def _zope_dottedname_style(self, value, package): + """ package.module.attr style """ + module = getattr(package, '__name__', None) # package may be None + if not module: + module = None + if value == '.': + if module is None: + raise ValueError( + 'relative name %r irresolveable without package' % (value,) + ) + name = module.split('.') + else: + name = value.split('.') + if not name[0]: + if module is None: + raise ValueError( + 'relative name %r irresolveable without ' + 'package' % (value,) + ) + module = module.split('.') + name.pop(0) + while not name[0]: + module.pop() + name.pop(0) + name = module + name + + used = name.pop(0) + found = __import__(used) + for n in name: + used += '.' + n + try: + found = getattr(found, n) + except AttributeError: + __import__(used) + found = getattr(found, n) # pragma: no cover + + return found + +@implementer(IAssetDescriptor) +class PkgResourcesAssetDescriptor(object): + pkg_resources = pkg_resources + + def __init__(self, pkg_name, path): + self.pkg_name = pkg_name + self.path = path + + def absspec(self): + return '%s:%s' % (self.pkg_name, self.path) + + def abspath(self): + return self.pkg_resources.resource_filename(self.pkg_name, self.path) + + def stream(self): + return self.pkg_resources.resource_stream(self.pkg_name, self.path) + + def isdir(self): + return self.pkg_resources.resource_isdir(self.pkg_name, self.path) + + def listdir(self): + return self.pkg_resources.resource_listdir(self.pkg_name, self.path) + + def exists(self): + return self.pkg_resources.resource_exists(self.pkg_name, self.path) + +@implementer(IAssetDescriptor) +class FSAssetDescriptor(object): + + def __init__(self, path): + self.path = os.path.abspath(path) + + def absspec(self): + raise NotImplementedError + + def abspath(self): + return self.path + + def stream(self): + return open(self.path, 'rb') + + def isdir(self): + return os.path.isdir(self.path) + + def listdir(self): + return os.listdir(self.path) + + def exists(self): + return os.path.exists(self.path) diff --git a/pyramid/registry.py b/pyramid/registry.py index ac706595e..7e373b58a 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,7 +1,16 @@ +import operator + +from zope.interface import implementer + from zope.interface.registry import Components from pyramid.compat import text_ -from pyramid.interfaces import ISettings + +from pyramid.interfaces import ( + ISettings, + IIntrospector, + IIntrospectable, + ) empty = text_('') @@ -26,6 +35,7 @@ class Registry(Components, dict): # for optimization purposes, if no listeners are listening, don't try # to notify them has_listeners = False + _settings = None def __nonzero__(self): @@ -74,4 +84,163 @@ class Registry(Components, dict): settings = property(_get_settings, _set_settings) +@implementer(IIntrospector) +class Introspector(object): + def __init__(self): + self._refs = {} + self._categories = {} + self._counter = 0 + + def add(self, intr): + category = self._categories.setdefault(intr.category_name, {}) + category[intr.discriminator] = intr + category[intr.discriminator_hash] = intr + intr.order = self._counter + self._counter += 1 + + def get(self, category_name, discriminator, default=None): + category = self._categories.setdefault(category_name, {}) + intr = category.get(discriminator, default) + return intr + + def get_category(self, category_name, default=None, sort_key=None): + if sort_key is None: + sort_key = operator.attrgetter('order') + category = self._categories.get(category_name) + if category is None: + return default + values = category.values() + values = sorted(set(values), key=sort_key) + return [ + {'introspectable':intr, + 'related':self.related(intr)} + for intr in values + ] + + def categorized(self, sort_key=None): + L = [] + for category_name in self.categories(): + L.append((category_name, self.get_category(category_name, + sort_key=sort_key))) + return L + + def categories(self): + return sorted(self._categories.keys()) + + def remove(self, category_name, discriminator): + intr = self.get(category_name, discriminator) + if intr is None: + return + L = self._refs.pop(intr, []) + for d in L: + L2 = self._refs[d] + L2.remove(intr) + category = self._categories[intr.category_name] + del category[intr.discriminator] + del category[intr.discriminator_hash] + + def _get_intrs_by_pairs(self, pairs): + introspectables = [] + for pair in pairs: + category_name, discriminator = pair + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + introspectables.append(intr) + return introspectables + + def relate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.setdefault(x, []) + if x is not y and y not in L: + L.append(y) + + def unrelate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.get(x, []) + if y in L: + L.remove(y) + + def related(self, intr): + category_name, discriminator = intr.category_name, intr.discriminator + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + return self._refs.get(intr, []) + +@implementer(IIntrospector) +class _NoopIntrospector(object): + def add(self, intr): + pass + def get(self, category_name, discriminator, default=None): + return default + def get_category(self, category_name, default=None, sort_key=None): + return default + def categorized(self, sort_key=None): + return [] + def categories(self): + return [] + def remove(self, category_name, discriminator): + return + def relate(self, *pairs): + return + unrelate = relate + def related(self, intr): + return [] + +noop_introspector = _NoopIntrospector() + +@implementer(IIntrospectable) +class Introspectable(dict): + + order = 0 # mutated by introspector.add + action_info = None # mutated by self.register + + def __init__(self, category_name, discriminator, title, type_name): + self.category_name = category_name + self.discriminator = discriminator + self.title = title + self.type_name = type_name + self._relations = [] + + def relate(self, category_name, discriminator): + self._relations.append((True, category_name, discriminator)) + + def unrelate(self, category_name, discriminator): + self._relations.append((False, category_name, discriminator)) + + @property + def discriminator_hash(self): + return hash(self.discriminator) + + def __hash__(self): + return hash((self.category_name,) + (self.discriminator,)) + + def __repr__(self): + return '<%s category %r, discriminator %r>' % (self.__class__.__name__, + self.category_name, + self.discriminator) + + def __nonzero__(self): + return True + + __bool__ = __nonzero__ # py3 + + def register(self, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for relate, category_name, discriminator in self._relations: + if relate: + method = introspector.relate + else: + method = introspector.unrelate + method( + (self.category_name, self.discriminator), + (category_name, discriminator) + ) + global_registry = Registry('global') diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 073ce444d..61f5e0b35 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,26 +1,36 @@ +import json import os import pkg_resources import threading from zope.interface import implementer -from zope.deprecation import deprecated -from pyramid.interfaces import IChameleonLookup -from pyramid.interfaces import IChameleonTranslate -from pyramid.interfaces import IRendererGlobalsFactory -from pyramid.interfaces import IRendererFactory -from pyramid.interfaces import IResponseFactory -from pyramid.interfaces import ITemplateRenderer -from pyramid.interfaces import IRendererInfo +from pyramid.interfaces import ( + IChameleonLookup, + IChameleonTranslate, + IRendererGlobalsFactory, + IRendererFactory, + IResponseFactory, + ITemplateRenderer, + IRendererInfo, + ) from pyramid.asset import asset_spec_from_abspath -from pyramid.compat import json -from pyramid.compat import string_types -from pyramid.compat import text_type + +from pyramid.compat import ( + string_types, + text_type, + ) + from pyramid.decorator import reify + from pyramid.events import BeforeRender -from pyramid.path import caller_package -from pyramid.path import package_path + +from pyramid.path import ( + caller_package, + package_path, + ) + from pyramid.response import Response from pyramid.threadlocal import get_current_registry @@ -340,16 +350,6 @@ def template_renderer_factory(info, impl, lock=registry_lock): lock.release() return lookup(info) -def renderer_from_name(path, package=None): - return RendererHelper(name=path, package=package).renderer - -deprecated( - 'renderer_from_name', - 'The "pyramid.renderers.renderer_from_name" function was never an API. ' - 'However, its use has been observed "in the wild." It will disappear in ' - 'the next major release. To replace it, use the ' - '``pyramid.renderers.get_renderer`` API instead. ') - @implementer(IRendererInfo) class RendererHelper(object): def __init__(self, name=None, package=None, registry=None): diff --git a/pyramid/request.py b/pyramid/request.py index d98175feb..4005213fa 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -1,3 +1,5 @@ +import json + from zope.deprecation import deprecate from zope.deprecation.deprecation import deprecated from zope.interface import implementer @@ -5,17 +7,22 @@ from zope.interface.interface import InterfaceClass from webob import BaseRequest -from pyramid.interfaces import IRequest -from pyramid.interfaces import IResponse -from pyramid.interfaces import ISessionFactory -from pyramid.interfaces import IResponseFactory - -from pyramid.compat import json -from pyramid.compat import iterkeys_, itervalues_, iteritems_ -from pyramid.compat import text_ -from pyramid.compat import bytes_ -from pyramid.compat import native_ -from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IRequest, + IResponse, + ISessionFactory, + IResponseFactory, + ) + +from pyramid.compat import ( + iterkeys_, + itervalues_, + iteritems_, + text_, + bytes_, + native_, + ) + from pyramid.decorator import reify from pyramid.response import Response from pyramid.url import URLMethodsMixin @@ -132,9 +139,14 @@ class DeprecatedRequestMethodsMixin(object): response_headerlist = property(_response_headerlist_get, _response_headerlist_set, _response_headerlist_del) - response_headerlist = deprecated( - response_headerlist, - rr_dep % ('headerlist', 'headerlist')) + + hl_dep = ('Accessing and setting "request.response_headerlist" is ' + 'deprecated as of Pyramid 1.1; access the headerlist via ' + '"request.response.headerlist" and extend headers via ' + '"request.response.headerlist.extend(alist)" instead of ' + '"request.response_headerlist = alist"') + + response_headerlist = deprecated(response_headerlist, hl_dep) # response_status def _response_status_get(self): diff --git a/pyramid/router.py b/pyramid/router.py index fb309eb03..0c115a1ac 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,26 +1,37 @@ -from zope.interface import implementer -from zope.interface import providedBy - -from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IRequest -from pyramid.interfaces import IRootFactory -from pyramid.interfaces import IRouteRequest -from pyramid.interfaces import IRouter -from pyramid.interfaces import IRequestFactory -from pyramid.interfaces import IRoutesMapper -from pyramid.interfaces import ITraverser -from pyramid.interfaces import IView -from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import ITweens - -from pyramid.events import ContextFound -from pyramid.events import NewRequest -from pyramid.events import NewResponse +from zope.interface import ( + implementer, + providedBy, + ) + +from pyramid.interfaces import ( + IDebugLogger, + IRequest, + IRootFactory, + IRouteRequest, + IRouter, + IRequestFactory, + IRoutesMapper, + ITraverser, + IView, + IViewClassifier, + ITweens, + ) + +from pyramid.events import ( + ContextFound, + NewRequest, + NewResponse, + ) + from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager -from pyramid.traversal import DefaultRootFactory -from pyramid.traversal import ResourceTreeTraverser + +from pyramid.traversal import ( + DefaultRootFactory, + ResourceTreeTraverser, + ) + from pyramid.tweens import excview_tween_factory @implementer(IRouter) @@ -178,7 +189,7 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - + return response(request.environ, start_response) finally: diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py index 4fecfc1e7..1906e0f51 100644 --- a/pyramid/scaffolds/__init__.py +++ b/pyramid/scaffolds/__init__.py @@ -7,6 +7,10 @@ from pyramid.scaffolds.template import Template class PyramidTemplate(Template): def pre(self, command, output_dir, vars): + if vars['package'] == 'site': + raise ValueError('Sorry, you may not name your package "site". ' + 'The package name "site" has a special meaning in ' + 'Python. Please name it anything except "site".') vars['random_string'] = native_(binascii.hexlify(os.urandom(20))) package_logger = vars['package'] if package_logger == 'root': @@ -28,13 +32,17 @@ class StarterProjectTemplate(PyramidTemplate): class ZODBProjectTemplate(PyramidTemplate): _template_dir = 'zodb' - summary = 'Pyramid ZODB starter project' - -class RoutesAlchemyProjectTemplate(PyramidTemplate): - _template_dir = 'routesalchemy' - summary = 'Pyramid SQLAlchemy project using url dispatch (no traversal)' + summary = 'Pyramid ZODB project using traversal' class AlchemyProjectTemplate(PyramidTemplate): _template_dir = 'alchemy' - summary = 'Pyramid SQLAlchemy project using traversal' - + summary = 'Pyramid SQLAlchemy project using url dispatch' + def post(self, command, output_dir, vars): # pragma: no cover + val = PyramidTemplate.post(self, command, output_dir, vars) + self.out('') + self.out('Please run the "populate_%(project)s" script to set up the ' + 'SQL database after installing (but before starting) the ' + 'application (e.g. ' + '"$myvirtualenv/bin/populate_%(project)s development.ini".)' + % vars) + return val diff --git a/pyramid/scaffolds/routesalchemy/+package+/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/__init__.py index 24201912b..253341563 100644 --- a/pyramid/scaffolds/routesalchemy/+package+/__init__.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/__init__.py @@ -1,18 +1,16 @@ from pyramid.config import Configurator from sqlalchemy import engine_from_config -from {{package}}.models import initialize_sql +from .models import DBSession def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) + DBSession.configure(bind=engine) config = Configurator(settings=settings) - config.add_static_view('static', '{{package}}:static', cache_max_age=3600) + config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') - config.add_view('{{package}}.views.my_view', - route_name='home', - renderer='templates/mytemplate.pt') + config.scan() return config.make_wsgi_app() diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/__init__.py_tmpl deleted file mode 100755 index 8eb878688..000000000 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py_tmpl +++ /dev/null @@ -1,19 +0,0 @@ -from pyramid.config import Configurator -from sqlalchemy import engine_from_config - -from {{package}}.models import appmaker - -def main(global_config, **settings): - """ This function returns a WSGI application. - """ - engine = engine_from_config(settings, 'sqlalchemy.') - get_root = appmaker(engine) - config = Configurator(settings=settings, root_factory=get_root) - config.add_static_view('static', '{{package}}:static', cache_max_age=3600) - config.add_view('{{package}}.views.view_root', - context='{{package}}.models.MyRoot', - renderer="templates/root.pt") - config.add_view('{{package}}.views.view_model', - context='{{package}}.models.MyModel', - renderer="templates/model.pt") - return config.make_wsgi_app() diff --git a/pyramid/scaffolds/alchemy/+package+/models.py b/pyramid/scaffolds/alchemy/+package+/models.py index 2685da5bb..b6ac15429 100755..100644 --- a/pyramid/scaffolds/alchemy/+package+/models.py +++ b/pyramid/scaffolds/alchemy/+package+/models.py @@ -1,15 +1,15 @@ -import transaction - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker +from sqlalchemy import ( + Column, + Integer, + Text, + ) from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.exc import IntegrityError - -from sqlalchemy import Integer -from sqlalchemy import Unicode -from sqlalchemy import Column +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) from zope.sqlalchemy import ZopeTransactionExtension @@ -19,66 +19,10 @@ Base = declarative_base() class MyModel(Base): __tablename__ = 'models' id = Column(Integer, primary_key=True) - name = Column(Unicode(255), unique=True) + name = Column(Text, unique=True) value = Column(Integer) def __init__(self, name, value): self.name = name self.value = value -class MyRoot(object): - __name__ = None - __parent__ = None - - def __getitem__(self, key): - session= DBSession() - try: - id = int(key) - except (ValueError, TypeError): - raise KeyError(key) - - item = session.query(MyModel).get(id) - if item is None: - raise KeyError(key) - - item.__parent__ = self - item.__name__ = key - return item - - def get(self, key, default=None): - try: - item = self.__getitem__(key) - except KeyError: - item = default - return item - - def __iter__(self): - session= DBSession() - query = session.query(MyModel) - return iter(query) - -root = MyRoot() - -def root_factory(request): - return root - -def populate(): - session = DBSession() - model = MyModel(name='test name', value=55) - session.add(model) - session.flush() - transaction.commit() - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - populate() - except IntegrityError: - transaction.abort() - return DBSession - -def appmaker(engine): - initialize_sql(engine) - return root_factory diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py b/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/populate.py b/pyramid/scaffolds/alchemy/+package+/scripts/populate.py new file mode 100644 index 000000000..0e828465f --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/scripts/populate.py @@ -0,0 +1,35 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from ..models import ( + DBSession, + MyModel, + Base, + ) + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=1) + DBSession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/templates/model.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/model.pt_tmpl deleted file mode 100644 index 83ddd768e..000000000 --- a/pyramid/scaffolds/alchemy/+package+/templates/model.pt_tmpl +++ /dev/null @@ -1,82 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('{{package}}:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('{{package}}:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> - <br/> - <p> - <b>Id:</b> ${item.id}<br /> - <b>Name:</b> ${item.name}<br /> - <b>Value:</b> ${item.value} - </p> - </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> - </ul> - </div> - </div> - </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl index 3cd9c66a4..ac0140789 100644 --- a/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl @@ -32,7 +32,7 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html"> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/search.html"> <input type="text" id="q" name="q" value="" /> <input type="submit" id="x" value="Go" /> </form> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> diff --git a/pyramid/scaffolds/alchemy/+package+/templates/root.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/root.pt_tmpl deleted file mode 100644 index fc41ce20a..000000000 --- a/pyramid/scaffolds/alchemy/+package+/templates/root.pt_tmpl +++ /dev/null @@ -1,80 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('{{package}}:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('{{package}}:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/current/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> - <br/> - <p tal:repeat="item items"> - <a href="${item.id}">${item.name}</a> - </p> - </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> - </ul> - </div> - </div> - </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 83b6a4739..729b337cb 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -1,59 +1,32 @@ import unittest +import transaction from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from {{package}}.models import initialize_sql - session = initialize_sql(create_engine('sqlite://')) - return session +from .models import DBSession -class TestMyRoot(unittest.TestCase): +class TestMyView(unittest.TestCase): def setUp(self): self.config = testing.setUp() - self.session = _initTestingDB() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = MyModel(name='one', value=55) + DBSession.add(model) def tearDown(self): + DBSession.remove() testing.tearDown() - self.session.remove() - def _makeOne(self): - from {{package}}.models import MyRoot - return MyRoot() - - def test___getitem__hit(self): - from {{package}}.models import MyModel - root = self._makeOne() - first = root['1'] - self.assertEqual(first.__class__, MyModel) - self.assertEqual(first.__parent__, root) - self.assertEqual(first.__name__, '1') - - def test___getitem__miss(self): - root = self._makeOne() - self.assertRaises(KeyError, root.__getitem__, '100') - - def test___getitem__notint(self): - root = self._makeOne() - self.assertRaises(KeyError, root.__getitem__, 'notint') - - def test_get_hit(self): - from {{package}}.models import MyModel - root = self._makeOne() - first = root.get('1') - self.assertEqual(first.__class__, MyModel) - self.assertEqual(first.__parent__, root) - self.assertEqual(first.__name__, '1') - - def test_get_miss(self): - root = self._makeOne() - self.assertEqual(root.get('100', 'default'), 'default') - self.assertEqual(root.get('100'), None) - - def test___iter__(self): - root = self._makeOne() - iterable = iter(root) - result = list(iterable) - self.assertEqual(len(result), 1) - model = result[0] - self.assertEqual(model.id, 1) + def test_it(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], '{{project}}') diff --git a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views.py_tmpl index 12bce138e..b7b23fc67 100644 --- a/pyramid/scaffolds/alchemy/+package+/views.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/views.py_tmpl @@ -1,5 +1,11 @@ -def view_root(context, request): - return {'items':list(context), 'project':'{{project}}'} +from pyramid.view import view_config -def view_model(context, request): - return {'item':context, 'project':'{{project}}'} +from .models import ( + DBSession, + MyModel, + ) + +@view_config(route_name='home', renderer='templates/mytemplate.pt') +def my_view(request): + one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + return {'one':one, 'project':'{{project}}'} diff --git a/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl b/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl index 5b34f7803..35a34f332 100644 --- a/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl +++ b/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl @@ -1,4 +1,4 @@ -0.1 +0.0 --- -- Initial version +- Initial version diff --git a/pyramid/scaffolds/alchemy/README.txt_tmpl b/pyramid/scaffolds/alchemy/README.txt_tmpl index 40f98d14a..efea71c5c 100644 --- a/pyramid/scaffolds/alchemy/README.txt_tmpl +++ b/pyramid/scaffolds/alchemy/README.txt_tmpl @@ -1 +1,14 @@ {{project}} README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/populate_{{project}} development.ini + +- $venv/bin/pserve development.ini + diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index 68f8e6245..a2766547d 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -9,9 +9,10 @@ CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() requires = [ 'pyramid', + 'SQLAlchemy', + 'transaction', 'pyramid_tm', 'pyramid_debugtoolbar', - 'sqlalchemy', 'zope.sqlalchemy', ] @@ -31,16 +32,17 @@ setup(name='{{project}}', author='', author_email='', url='', - keywords='web pylons pyramid', + keywords='web wsgi bfg pylons pyramid', packages=find_packages(), include_package_data=True, zip_safe=False, + test_suite='{{package}}', install_requires = requires, - tests_require = requires, - test_suite="{{package}}", entry_points = """\ [paste.app_factory] main = {{package}}:main + [console_scripts] + populate_{{project}} = {{package}}.scripts.populate:main """, ) diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py index e0badb26e..c99238d33 100644 --- a/pyramid/scaffolds/copydir.py +++ b/pyramid/scaffolds/copydir.py @@ -10,7 +10,9 @@ import urllib from pyramid.compat import ( input_, - native_ + native_, + url_quote as compat_url_quote, + escape, ) fsenc = sys.getfilesystemencoding() @@ -79,7 +81,7 @@ def copy_dir(source, dest, vars, verbosity, simulate, indent=0, if verbosity >= 2: reason = pad + reason % {'filename': full} out(reason) - continue + continue # pragma: no cover if sub_vars: dest_full = os.path.join(dest, substitute_filename(name, vars)) sub_file = False @@ -114,10 +116,10 @@ def copy_dir(source, dest, vars, verbosity, simulate, indent=0, content, vars, filename=full, template_renderer=template_renderer ) - except SkipTemplate: - continue - if content is None: - continue + except SkipTemplate: + continue # pragma: no cover + if content is None: + continue # pragma: no cover already_exists = os.path.exists(dest_full) if already_exists: f = open(dest_full, 'rb') @@ -127,7 +129,7 @@ def copy_dir(source, dest, vars, verbosity, simulate, indent=0, if verbosity: out('%s%s already exists (same content)' % (pad, dest_full)) - continue + continue # pragma: no cover if interactive: if not query_interactive( native_(full, fsenc), native_(dest_full, fsenc), @@ -135,7 +137,7 @@ def copy_dir(source, dest, vars, verbosity, simulate, indent=0, simulate=simulate, out_=out_): continue elif not overwrite: - continue + continue # pragma: no cover if verbosity and use_pkg_resources: out('%sCopying %s to %s' % (pad, full, dest_full)) elif verbosity: @@ -159,7 +161,7 @@ def should_skip_file(name): if name.endswith('~') or name.endswith('.bak'): return 'Skipping backup file %(filename)s' if name.endswith('.pyc') or name.endswith('.pyo'): - return 'Skipping %s file %(filename)s' % os.path.splitext(name)[1] + return 'Skipping %s file ' % os.path.splitext(name)[1] + '%(filename)s' if name.endswith('$py.class'): return 'Skipping $py.class file %(filename)s' if name in ('CVS', '_darcs'): @@ -262,12 +264,12 @@ def substitute_content(content, vars, filename='<string>', def html_quote(s): if s is None: return '' - return cgi.escape(str(s), 1) + return escape(str(s), 1) def url_quote(s): if s is None: return '' - return urllib.quote(str(s)) + return compat_url_quote(str(s)) def test(conf, true_cond, false_cond=None): if conf: diff --git a/pyramid/scaffolds/routesalchemy/+package+/models.py b/pyramid/scaffolds/routesalchemy/+package+/models.py deleted file mode 100644 index bef483d3a..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/models.py +++ /dev/null @@ -1,42 +0,0 @@ -import transaction - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Unicode - -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Unicode(255), unique=True) - value = Column(Integer) - - def __init__(self, name, value): - self.name = name - self.value = value - -def populate(): - session = DBSession() - model = MyModel(name='root', value=55) - session.add(model) - session.flush() - transaction.commit() - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - populate() - except IntegrityError: - transaction.abort() diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/favicon.ico b/pyramid/scaffolds/routesalchemy/+package+/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/favicon.ico +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/footerbg.png b/pyramid/scaffolds/routesalchemy/+package+/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/footerbg.png +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/headerbg.png b/pyramid/scaffolds/routesalchemy/+package+/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/headerbg.png +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/ie6.css b/pyramid/scaffolds/routesalchemy/+package+/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/middlebg.png b/pyramid/scaffolds/routesalchemy/+package+/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/middlebg.png +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/pylons.css b/pyramid/scaffolds/routesalchemy/+package+/static/pylons.css deleted file mode 100644 index c54499ddd..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "NobileRegular","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"NeutonRegular","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/pyramid.png b/pyramid/scaffolds/routesalchemy/+package+/static/pyramid.png Binary files differdeleted file mode 100644 index 347e05549..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/pyramid.png +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/static/transparent.gif b/pyramid/scaffolds/routesalchemy/+package+/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/static/transparent.gif +++ /dev/null diff --git a/pyramid/scaffolds/routesalchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/routesalchemy/+package+/tests.py_tmpl deleted file mode 100644 index 29aea7258..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/tests.py_tmpl +++ /dev/null @@ -1,24 +0,0 @@ -import unittest -from pyramid.config import Configurator -from pyramid import testing - -def _initTestingDB(): - from sqlalchemy import create_engine - from {{package}}.models import initialize_sql - session = initialize_sql(create_engine('sqlite://')) - return session - -class TestMyView(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - _initTestingDB() - - def tearDown(self): - testing.tearDown() - - def test_it(self): - from {{package}}.views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['root'].name, 'root') - self.assertEqual(info['project'], '{{project}}') diff --git a/pyramid/scaffolds/routesalchemy/+package+/views.py_tmpl b/pyramid/scaffolds/routesalchemy/+package+/views.py_tmpl deleted file mode 100644 index 45532b47b..000000000 --- a/pyramid/scaffolds/routesalchemy/+package+/views.py_tmpl +++ /dev/null @@ -1,7 +0,0 @@ -from {{package}}.models import DBSession -from {{package}}.models import MyModel - -def my_view(request): - dbsession = DBSession() - root = dbsession.query(MyModel).filter(MyModel.name=='root').first() - return {'root':root, 'project':'{{project}}'} diff --git a/pyramid/scaffolds/routesalchemy/CHANGES.txt_tmpl b/pyramid/scaffolds/routesalchemy/CHANGES.txt_tmpl deleted file mode 100644 index 35a34f332..000000000 --- a/pyramid/scaffolds/routesalchemy/CHANGES.txt_tmpl +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version diff --git a/pyramid/scaffolds/routesalchemy/MANIFEST.in_tmpl b/pyramid/scaffolds/routesalchemy/MANIFEST.in_tmpl deleted file mode 100644 index 0ff6eb7a0..000000000 --- a/pyramid/scaffolds/routesalchemy/MANIFEST.in_tmpl +++ /dev/null @@ -1,2 +0,0 @@ -include *.txt *.ini *.cfg *.rst -recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/pyramid/scaffolds/routesalchemy/README.txt_tmpl b/pyramid/scaffolds/routesalchemy/README.txt_tmpl deleted file mode 100644 index 40f98d14a..000000000 --- a/pyramid/scaffolds/routesalchemy/README.txt_tmpl +++ /dev/null @@ -1 +0,0 @@ -{{project}} README diff --git a/pyramid/scaffolds/routesalchemy/development.ini_tmpl b/pyramid/scaffolds/routesalchemy/development.ini_tmpl deleted file mode 100644 index d804a0b0e..000000000 --- a/pyramid/scaffolds/routesalchemy/development.ini_tmpl +++ /dev/null @@ -1,57 +0,0 @@ -[app:main] -use = egg:{{project}} - -pyramid.reload_templates = true -pyramid.debug_authorization = false -pyramid.debug_notfound = false -pyramid.debug_routematch = false -pyramid.debug_templates = true -pyramid.default_locale_name = en -pyramid.includes = pyramid_debugtoolbar - pyramid_tm - -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db - -[server:main] -use = egg:pyramid#wsgiref -host = 0.0.0.0 -port = 6543 - -# Begin logging configuration - -[loggers] -keys = root, {{package_logger}}, sqlalchemy - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = INFO -handlers = console - -[logger_{{package_logger}}] -level = DEBUG -handlers = -qualname = {{package}} - -[logger_sqlalchemy] -level = INFO -handlers = -qualname = sqlalchemy.engine -# "level = INFO" logs SQL queries. -# "level = DEBUG" logs SQL queries and results. -# "level = WARN" logs neither. (Recommended for production systems.) - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/routesalchemy/production.ini_tmpl b/pyramid/scaffolds/routesalchemy/production.ini_tmpl deleted file mode 100644 index 7350ce25f..000000000 --- a/pyramid/scaffolds/routesalchemy/production.ini_tmpl +++ /dev/null @@ -1,56 +0,0 @@ -[app:main] -use = egg:{{project}} - -pyramid.reload_templates = false -pyramid.debug_authorization = false -pyramid.debug_notfound = false -pyramid.debug_routematch = false -pyramid.debug_templates = false -pyramid.default_locale_name = en -pyramid.includes = pyramid_tm - -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db - -[server:main] -use = egg:pyramid#wsgiref -host = 0.0.0.0 -port = 6543 - -# Begin logging configuration - -[loggers] -keys = root, {{package_logger}}, sqlalchemy - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console - -[logger_{{package_logger}}] -level = WARN -handlers = -qualname = {{package}} - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine -# "level = INFO" logs SQL queries. -# "level = DEBUG" logs SQL queries and results. -# "level = WARN" logs neither. (Recommended for production systems.) - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/routesalchemy/setup.cfg_tmpl b/pyramid/scaffolds/routesalchemy/setup.cfg_tmpl deleted file mode 100644 index 5bec29823..000000000 --- a/pyramid/scaffolds/routesalchemy/setup.cfg_tmpl +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package={{package}} -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = {{package}}/locale -domain = {{project}} -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = {{package}}/locale/{{project}}.pot -width = 80 - -[init_catalog] -domain = {{project}} -input_file = {{package}}/locale/{{project}}.pot -output_dir = {{package}}/locale - -[update_catalog] -domain = {{project}} -input_file = {{package}}/locale/{{project}}.pot -output_dir = {{package}}/locale -previous = true diff --git a/pyramid/scaffolds/routesalchemy/setup.py_tmpl b/pyramid/scaffolds/routesalchemy/setup.py_tmpl deleted file mode 100644 index a2cdaac60..000000000 --- a/pyramid/scaffolds/routesalchemy/setup.py_tmpl +++ /dev/null @@ -1,46 +0,0 @@ -import os -import sys - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() - -requires = [ - 'pyramid', - 'SQLAlchemy', - 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', - 'zope.sqlalchemy', - ] - -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - -setup(name='{{project}}', - version='0.0', - description='{{project}}', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], - author='', - author_email='', - url='', - keywords='web wsgi bfg pylons pyramid', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - test_suite='{{package}}', - install_requires = requires, - entry_points = """\ - [paste.app_factory] - main = {{package}}:main - """, - ) - diff --git a/pyramid/scaffolds/starter/+package+/__init__.py b/pyramid/scaffolds/starter/+package+/__init__.py new file mode 100644 index 000000000..31b02cf02 --- /dev/null +++ b/pyramid/scaffolds/starter/+package+/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() + return config.make_wsgi_app() diff --git a/pyramid/scaffolds/starter/+package+/__init__.py_tmpl b/pyramid/scaffolds/starter/+package+/__init__.py_tmpl deleted file mode 100644 index d763b2435..000000000 --- a/pyramid/scaffolds/starter/+package+/__init__.py_tmpl +++ /dev/null @@ -1,12 +0,0 @@ -from pyramid.config import Configurator -from {{package}}.resources import Root - -def main(global_config, **settings): - """ This function returns a Pyramid WSGI application. - """ - config = Configurator(root_factory=Root, settings=settings) - config.add_view('{{package}}.views.my_view', - context='{{package}}:resources.Root', - renderer='{{package}}:templates/mytemplate.pt') - config.add_static_view('static', '{{package}}:static', cache_max_age=3600) - return config.make_wsgi_app() diff --git a/pyramid/scaffolds/starter/+package+/resources.py b/pyramid/scaffolds/starter/+package+/resources.py deleted file mode 100644 index 3d811895c..000000000 --- a/pyramid/scaffolds/starter/+package+/resources.py +++ /dev/null @@ -1,3 +0,0 @@ -class Root(object): - def __init__(self, request): - self.request = request diff --git a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt index 3cd9c66a4..0bfac946e 100644 --- a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('{{package}}:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('{{package}}:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> diff --git a/pyramid/scaffolds/starter/+package+/tests.py_tmpl b/pyramid/scaffolds/starter/+package+/tests.py_tmpl index 1627bf015..401260efb 100644 --- a/pyramid/scaffolds/starter/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/starter/+package+/tests.py_tmpl @@ -10,7 +10,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from {{package}}.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], '{{project}}') diff --git a/pyramid/scaffolds/starter/+package+/views.py_tmpl b/pyramid/scaffolds/starter/+package+/views.py_tmpl index 12ed8832d..7a0d2f386 100644 --- a/pyramid/scaffolds/starter/+package+/views.py_tmpl +++ b/pyramid/scaffolds/starter/+package+/views.py_tmpl @@ -1,2 +1,5 @@ +from pyramid.view import view_config + +@view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): return {'project':'{{project}}'} diff --git a/pyramid/scaffolds/tests.py b/pyramid/scaffolds/tests.py index 98f84da73..04e802396 100644 --- a/pyramid/scaffolds/tests.py +++ b/pyramid/scaffolds/tests.py @@ -31,9 +31,6 @@ class TemplateTest(object): self.directory = tempfile.mkdtemp() self.make_venv(self.directory) os.chdir(pkg_resources.get_distribution('pyramid').location) - subprocess.check_call( # XXX remove later - [os.path.join(self.directory, 'bin', 'easy_install'), - 'https://github.com/Pylons/webob/zipball/master']) subprocess.check_call( [os.path.join(self.directory, 'bin', 'python'), 'setup.py', 'develop']) @@ -42,6 +39,10 @@ class TemplateTest(object): os.chdir('Dingle') py = os.path.join(self.directory, 'bin', 'python') subprocess.check_call([py, 'setup.py', 'install']) + if tmpl_name == 'alchemy': + populate = os.path.join(self.directory, 'bin', + 'populate_Dingle') + subprocess.check_call([populate, 'development.ini']) subprocess.check_call([py, 'setup.py', 'test']) pserve = os.path.join(self.directory, 'bin', 'pserve') for ininame, hastoolbar in (('development.ini', True), @@ -69,7 +70,7 @@ class TemplateTest(object): os.chdir(self.old_cwd) if __name__ == '__main__': # pragma: no cover - templates = ['starter', 'alchemy', 'routesalchemy',] + templates = ['starter', 'alchemy',] if sys.version_info >= (2, 6) and sys.version_info < (3, 0): templates.append('zodb') diff --git a/pyramid/scaffolds/zodb/+package+/__init__.py_tmpl b/pyramid/scaffolds/zodb/+package+/__init__.py index 935121888..b63933fc5 100644 --- a/pyramid/scaffolds/zodb/+package+/__init__.py_tmpl +++ b/pyramid/scaffolds/zodb/+package+/__init__.py @@ -1,6 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from {{package}}.models import appmaker +from .models import appmaker def root_factory(request): conn = get_connection(request) @@ -10,6 +10,6 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ config = Configurator(root_factory=root_factory, settings=settings) - config.add_static_view('static', '{{package}}:static', cache_max_age=3600) - config.scan('{{package}}') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/pyramid/scaffolds/routesalchemy/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt index 3cd9c66a4..0bfac946e 100644 --- a/pyramid/scaffolds/routesalchemy/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt @@ -5,19 +5,19 @@ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('{{package}}:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('{{package}}:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('{{package}}:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="/static/pyramid.png" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#api-documentation">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/current/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> diff --git a/pyramid/scaffolds/zodb/+package+/tests.py_tmpl b/pyramid/scaffolds/zodb/+package+/tests.py_tmpl index 1627bf015..401260efb 100644 --- a/pyramid/scaffolds/zodb/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/zodb/+package+/tests.py_tmpl @@ -10,7 +10,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from {{package}}.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], '{{project}}') diff --git a/pyramid/scaffolds/zodb/+package+/views.py_tmpl b/pyramid/scaffolds/zodb/+package+/views.py_tmpl index d4a1147c6..083798095 100644 --- a/pyramid/scaffolds/zodb/+package+/views.py_tmpl +++ b/pyramid/scaffolds/zodb/+package+/views.py_tmpl @@ -1,6 +1,6 @@ from pyramid.view import view_config -from {{package}}.models import MyModel +from .models import MyModel -@view_config(context=MyModel, renderer='{{package}}:templates/mytemplate.pt') +@view_config(context=MyModel, renderer='templates/mytemplate.pt') def my_view(request): return {'project':'{{project}}'} diff --git a/pyramid/scripting.py b/pyramid/scripting.py index 47178c22e..f1dc24637 100644 --- a/pyramid/scripting.py +++ b/pyramid/scripting.py @@ -1,8 +1,12 @@ from pyramid.config import global_registries from pyramid.exceptions import ConfigurationError from pyramid.request import Request -from pyramid.interfaces import IRequestFactory -from pyramid.interfaces import IRootFactory + +from pyramid.interfaces import ( + IRequestFactory, + IRootFactory, + ) + from pyramid.threadlocal import manager as threadlocal_manager from pyramid.traversal import DefaultRootFactory diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 47a709af4..dacebd6ea 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -4,6 +4,7 @@ import optparse import os +import os.path import pkg_resources import re import sys @@ -23,6 +24,12 @@ class PCreateCommand(object): action='append', help=("Add a scaffold to the create process " "(multiple -s args accepted)")) + parser.add_option('-t', '--template', + dest='scaffold_name', + action='append', + help=('A backwards compatibility alias for ' + '-s/--scaffold. Add a scaffold to the ' + 'create process (multiple -t args accepted)')) parser.add_option('-l', '--list', dest='list', action='store_true', @@ -64,8 +71,8 @@ class PCreateCommand(object): def render_scaffolds(self): options = self.options args = self.args - project_name = args[0].lstrip(os.path.sep) - output_dir = os.path.normpath(os.path.join(os.getcwd(), project_name)) + project_name = os.path.basename(args[0]) + output_dir = os.path.abspath(os.path.normpath(args[0])) pkg_name = _bad_chars_re.sub('', project_name.lower()) safe_name = pkg_resources.safe_name(project_name) egg_name = pkg_resources.to_filename(safe_name) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 0e0b345a8..570417e95 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -1,7 +1,6 @@ import optparse import sys -from pyramid.compat import print_ from pyramid.paster import bootstrap def main(argv=sys.argv, quiet=False): @@ -42,7 +41,7 @@ class PRoutesCommand(object): def out(self, msg): # pragma: no cover if not self.quiet: - print_(msg) + print(msg) def run(self, quiet=False): if not self.args: @@ -65,14 +64,17 @@ class PRoutesCommand(object): self.out( fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View'))) for route in routes: + pattern = route.pattern + if not pattern.startswith('/'): + pattern = '/' + pattern request_iface = registry.queryUtility(IRouteRequest, name=route.name) view_callable = None if (request_iface is None) or (route.factory is not None): - self.out(fmt % (route.name, route.pattern, '<unknown>')) + self.out(fmt % (route.name, pattern, '<unknown>')) else: view_callable = registry.adapters.lookup( (IViewClassifier, request_iface, Interface), IView, name='', default=None) - self.out(fmt % (route.name, route.pattern, view_callable)) + self.out(fmt % (route.name, pattern, view_callable)) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 53e15c773..a0a3a8a70 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -22,7 +22,7 @@ import traceback from paste.deploy import loadapp, loadserver -from pyramid.scripts.common import logging_file_config +from pyramid.paster import setup_logging MAXFD = 1024 @@ -173,8 +173,8 @@ class PServeCommand(object): if self.verbose > 1: self.out('Running reloading file monitor') install_reloader(int(self.options.reload_interval)) - if self.requires_config_file: - watch_file(self.args[0]) + # if self.requires_config_file: + # watch_file(self.args[0]) else: return self.restart_with_reloader() @@ -266,7 +266,7 @@ class PServeCommand(object): log_fn = None if log_fn: log_fn = os.path.join(base, log_fn) - logging_file_config(log_fn) + setup_logging(log_fn) server = self.loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) @@ -843,3 +843,108 @@ def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover server = make_server(host, port, wsgi_app) print('Starting HTTP server on http://%s:%s' % (host, port)) server.serve_forever() + +# For paste.deploy server instantiation (egg:pyramid#cherrypy) +def cherrypy_server_runner( + app, global_conf=None, host='127.0.0.1', port=None, + ssl_pem=None, protocol_version=None, numthreads=None, + server_name=None, max=None, request_queue_size=None, + timeout=None + ): # pragma: no cover + """ + Entry point for CherryPy's WSGI server + + Serves the specified WSGI app via CherryPyWSGIServer. + + ``app`` + + The WSGI 'application callable'; multiple WSGI applications + may be passed as (script_name, callable) pairs. + + ``host`` + + This is the ipaddress to bind to (or a hostname if your + nameserver is properly configured). This defaults to + 127.0.0.1, which is not a public interface. + + ``port`` + + The port to run on, defaults to 8080 for HTTP, or 4443 for + HTTPS. This can be a string or an integer value. + + ``ssl_pem`` + + This an optional SSL certificate file (via OpenSSL) You can + generate a self-signed test PEM certificate file as follows: + + $ openssl genrsa 1024 > host.key + $ chmod 400 host.key + $ openssl req -new -x509 -nodes -sha1 -days 365 \\ + -key host.key > host.cert + $ cat host.cert host.key > host.pem + $ chmod 400 host.pem + + ``protocol_version`` + + The protocol used by the server, by default ``HTTP/1.1``. + + ``numthreads`` + + The number of worker threads to create. + + ``server_name`` + + The string to set for WSGI's SERVER_NAME environ entry. + + ``max`` + + The maximum number of queued requests. (defaults to -1 = no + limit). + + ``request_queue_size`` + + The 'backlog' argument to socket.listen(); specifies the + maximum number of queued connections. + + ``timeout`` + + The timeout in seconds for accepted connections. + """ + is_ssl = False + if ssl_pem: + port = port or 4443 + is_ssl = True + + if not port: + if ':' in host: + host, port = host.split(':', 1) + else: + port = 8080 + bind_addr = (host, int(port)) + + kwargs = {} + for var_name in ('numthreads', 'max', 'request_queue_size', 'timeout'): + var = locals()[var_name] + if var is not None: + kwargs[var_name] = int(var) + + from cherrypy import wsgiserver + + server = wsgiserver.CherryPyWSGIServer(bind_addr, app, + server_name=server_name, **kwargs) + server.ssl_certificate = server.ssl_private_key = ssl_pem + if protocol_version: + server.protocol = protocol_version + + try: + protocol = is_ssl and 'https' or 'http' + if host == '0.0.0.0': + print('serving on 0.0.0.0:%s view at %s://127.0.0.1:%s' % + (port, protocol, port)) + else: + print('serving on %s://%s:%s' % (protocol, host, port)) + server.start() + except (KeyboardInterrupt, SystemExit): + server.stop() + + return server diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 499d96aca..dfac9dbce 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -6,7 +6,7 @@ from pyramid.compat import configparser from pyramid.util import DottedNameResolver from pyramid.paster import bootstrap -from pyramid.scripts.common import logging_file_config +from pyramid.paster import setup_logging def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) @@ -36,10 +36,9 @@ class PShellCommand(object): summary = "Open an interactive shell with a Pyramid application loaded" parser = optparse.OptionParser() - parser.add_option('-d', '--disable-ipython', - action='store_true', - dest='disable_ipython', - help="Don't use IPython even if it is available") + parser.add_option('-p', '--python-shell', + action='store', type='string', dest='python_shell', + default='', help='ipython | bpython | python') parser.add_option('--setup', dest='setup', help=("A callable that will be passed the environment " @@ -86,7 +85,7 @@ class PShellCommand(object): return config_uri = self.args[0] config_file = config_uri.split('#', 1)[0] - logging_file_config(config_file) + setup_logging(config_file) self.pshell_file_config(config_file) # bootstrap the environ @@ -142,18 +141,36 @@ class PShellCommand(object): for var in sorted(self.object_help.keys()): help += '\n %-12s %s' % (var, self.object_help[var]) - if shell is None and not self.options.disable_ipython: + if shell is None: + shell = self.make_shell() + + try: + shell(env, help) + finally: + closer() + + def make_shell(self): + shell = None + user_shell = self.options.python_shell.lower() + if not user_shell: + shell = self.make_ipython_v0_11_shell() + if shell is None: + shell = self.make_ipython_v0_10_shell() + if shell is None: + shell = self.make_bpython_shell() + + elif user_shell == 'ipython': shell = self.make_ipython_v0_11_shell() if shell is None: shell = self.make_ipython_v0_10_shell() + elif user_shell == 'bpython': + shell = self.make_bpython_shell() + if shell is None: shell = self.make_default_shell() - try: - shell(env, help) - finally: - closer() + return shell def make_default_shell(self, interact=interact): def shell(env, help): @@ -163,6 +180,17 @@ class PShellCommand(object): interact(banner, local=env) return shell + def make_bpython_shell(self, BPShell=None): + if BPShell is None: # pragma: no cover + try: + from bpython import embed + BPShell = embed + except ImportError: + return None + def shell(env, help): + BPShell(locals_=env, banner=help + '\n') + return shell + def make_ipython_v0_11_shell(self, IPShellFactory=None): if IPShellFactory is None: # pragma: no cover try: diff --git a/pyramid/scripts/ptweens.py b/pyramid/scripts/ptweens.py index 81b4ae307..5bc0c7fbe 100644 --- a/pyramid/scripts/ptweens.py +++ b/pyramid/scripts/ptweens.py @@ -7,8 +7,6 @@ from pyramid.tweens import MAIN from pyramid.tweens import INGRESS from pyramid.paster import bootstrap -from pyramid.compat import print_ - def main(argv=sys.argv, quiet=False): command = PTweensCommand(argv, quiet) command.run() @@ -49,7 +47,7 @@ class PTweensCommand(object): def out(self, msg): # pragma: no cover if not self.quiet: - print_(msg) + print(msg) def show_chain(self, chain): fmt = '%-10s %-65s' diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 2ff9d6ed2..38d510542 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -1,7 +1,6 @@ import optparse import sys -from pyramid.compat import print_ from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap @@ -41,7 +40,7 @@ class PViewsCommand(object): def out(self, msg): # pragma: no cover if not self.quiet: - print_(msg) + print(msg) def _find_multi_routes(self, mapper, request): infos = [] diff --git a/pyramid/security.py b/pyramid/security.py index a552b613a..f29edd678 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -1,9 +1,11 @@ from zope.interface import providedBy -from pyramid.interfaces import IAuthenticationPolicy -from pyramid.interfaces import IAuthorizationPolicy -from pyramid.interfaces import ISecuredView -from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + ISecuredView, + IViewClassifier, + ) from pyramid.compat import map_ from pyramid.threadlocal import get_current_registry diff --git a/pyramid/session.py b/pyramid/session.py index a59f9c628..76b2b30b1 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -7,12 +7,16 @@ import os from zope.interface import implementer -from pyramid.compat import pickle -from pyramid.compat import PY3 -from pyramid.compat import text_ -from pyramid.compat import bytes_ -from pyramid.compat import native_ +from pyramid.compat import ( + pickle, + PY3, + text_, + bytes_, + native_, + ) + from pyramid.interfaces import ISession +from pyramid.util import strings_differ def manage_accessed(wrapped): """ Decorator which causes a cookie to be set when a wrapped @@ -262,17 +266,10 @@ def signed_deserialize(serialized, secret, hmac=hmac): sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - if len(sig) != len(input_sig): - raise ValueError('Wrong signature length') - # Avoid timing attacks (see # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - invalid_bits = 0 - for a, b in zip(sig, input_sig): - invalid_bits += a != b - - if invalid_bits: - raise ValueError('Invalid bits in signature') + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') return pickle.loads(pickled) diff --git a/pyramid/settings.py b/pyramid/settings.py index de91042eb..86304307e 100644 --- a/pyramid/settings.py +++ b/pyramid/settings.py @@ -26,6 +26,8 @@ deprecated( 'the ``settings`` attribute of the registry available from the request ' '(``request.registry.settings``)).') +truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) + def asbool(s): """ Return the boolean value ``True`` if the case-lowered value of string input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise @@ -34,18 +36,23 @@ def asbool(s): or ``False``, return it.""" if s is None: return False - if s in (True, False): + if isinstance(s, bool): return s s = str(s).strip() - return s.lower() in ('t', 'true', 'y', 'yes', 'on', '1') + return s.lower() in truthy def aslist_cronly(value): if isinstance(value, string_types): value = filter(None, [x.strip() for x in value.splitlines()]) return list(value) -def aslist(value): +def aslist(value, flatten=True): + """ Return a list of strings, separating the input based on newlines + and, if flatten=True (the default), also split on spaces within + each line.""" values = aslist_cronly(value) + if not flatten: + return values result = [] for value in values: subvalues = value.split() diff --git a/pyramid/static.py b/pyramid/static.py index 50a8b918b..61ee67573 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -1,23 +1,34 @@ # -*- coding: utf-8 -*- import mimetypes import os -from os.path import normcase -from os.path import normpath -from os.path import join -from os.path import getmtime -from os.path import getsize -from os.path import isdir -from os.path import exists -from pkg_resources import resource_exists -from pkg_resources import resource_filename -from pkg_resources import resource_isdir + +from os.path import ( + normcase, + normpath, + join, + getmtime, + getsize, + isdir, + exists, + ) + +from pkg_resources import ( + resource_exists, + resource_filename, + resource_isdir, + ) from repoze.lru import lru_cache from pyramid.asset import resolve_asset_spec + from pyramid.compat import text_ -from pyramid.httpexceptions import HTTPNotFound -from pyramid.httpexceptions import HTTPMovedPermanently + +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPMovedPermanently, + ) + from pyramid.path import caller_package from pyramid.response import Response from pyramid.traversal import traversal_path_info @@ -43,10 +54,12 @@ class _FileResponse(Response): def __init__(self, path, cache_max_age): super(_FileResponse, self).__init__(conditional_response=True) self.last_modified = getmtime(path) - content_type = mimetypes.guess_type(path, strict=False)[0] + content_type, content_encoding = mimetypes.guess_type(path, + strict=False) if content_type is None: content_type = 'application/octet-stream' self.content_type = content_type + self.content_encoding = content_encoding content_length = getsize(path) self.app_iter = _FileIter(open(path, 'rb'), content_length) # assignment of content_length must come after assignment of app_iter diff --git a/pyramid/testing.py b/pyramid/testing.py index e1011f5b4..025730ac5 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -3,32 +3,49 @@ import os from zope.deprecation import deprecated -from zope.interface import implementer -from zope.interface import Interface -from zope.interface import alsoProvides - -from pyramid.interfaces import IRequest -from pyramid.interfaces import IResponseFactory -from pyramid.interfaces import ISecuredView -from pyramid.interfaces import IView -from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import ISession - -from pyramid.compat import PY3 -from pyramid.compat import PYPY -from pyramid.compat import class_types +from zope.interface import ( + implementer, + Interface, + alsoProvides, + ) + +from pyramid.interfaces import ( + IRequest, + IResponseFactory, + ISecuredView, + IView, + IViewClassifier, + ISession, + ) + +from pyramid.compat import ( + PY3, + PYPY, + class_types, + ) + from pyramid.config import Configurator from pyramid.decorator import reify from pyramid.httpexceptions import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry -from pyramid.security import Authenticated -from pyramid.security import Everyone -from pyramid.security import has_permission -from pyramid.threadlocal import get_current_registry -from pyramid.threadlocal import manager -from pyramid.request import DeprecatedRequestMethodsMixin -from pyramid.request import CallbackMethodsMixin + +from pyramid.security import ( + Authenticated, + Everyone, + has_permission, + ) + +from pyramid.threadlocal import ( + get_current_registry, + manager, + ) + +from pyramid.request import ( + DeprecatedRequestMethodsMixin, + CallbackMethodsMixin, + ) + from pyramid.url import URLMethodsMixin _marker = object() @@ -620,7 +637,11 @@ class DummySession(dict): return token def get_csrf_token(self): - return self.get('_csrft_', None) + token = self.get('_csrft_', None) + if token is None: + token = self.new_csrf_token() + return token + @implementer(IRequest) class DummyRequest(DeprecatedRequestMethodsMixin, URLMethodsMixin, diff --git a/pyramid/tests/fixtures/static/arcs.svg.tgz b/pyramid/tests/fixtures/static/arcs.svg.tgz new file mode 100644 index 000000000..376c42ac8 --- /dev/null +++ b/pyramid/tests/fixtures/static/arcs.svg.tgz @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + id="canvas" + width="2000" + height="2000" + onload="init()"> + + <style type="text/css"> + .ellipse + { + stroke: red; + stroke-width: 2; + fill: blue; + fill-opacity: 0.1; + } + + .axes + { + stroke: blue; + stroke-width: 1; + } + </style> + + <script> + <![CDATA[ + var COLS = 4; + var ROWS = 4; + var RX = 80; + var RY = 30; + + function init() + { + var canvas = document.getElementById("canvas"); + + var angleStep = 360.0/(COLS*ROWS); + var spacing = 2*Math.max(RX, RY)+10; + for (var c = 0; c < COLS; ++c) { + for (var r = 0; r < ROWS; ++r) { + var ellipse = createEllipse((c+ COLS*r)*angleStep, spacing*(c+0.5), spacing*(r+0.5), RX, RY); + canvas.appendChild(ellipse); + } + } + } + + function createEllipse(phi, x, y, rx, ry) + { + var degPerRad = Math.PI/180.0; + var e1x = rx*Math.cos(phi*degPerRad); + var e1y = rx*Math.sin(phi*degPerRad); + var e2x = ry*Math.cos((phi+90)*degPerRad); + var e2y = ry*Math.sin((phi+90)*degPerRad); + + var axes = document.createElementNS("http://www.w3.org/2000/svg", "path"); + axes.setAttribute("class", "axes"); + axes.setAttribute("d", "M"+x+","+y+" l"+e1x+","+e1y+"M"+x+","+y+" l"+e2x+","+e2y); + var ellipse = document.createElementNS("http://www.w3.org/2000/svg", "path"); + ellipse.setAttribute("class", "ellipse"); + ellipse.setAttribute("d", "M" + (x+e1x) + "," + (y+e1y) + + "A" + rx + "," + ry + " " + phi + " 0,1 " + (x+e2x) + "," + (y+e2y) + + "A" + rx + "," + ry + " " + phi + " 1,1 " + (x+e1x) + "," + (y+e1y) +"z"); + + var group = document.createElementNS("http://www.w3.org/2000/svg", "g"); + group.appendChild(axes); + group.appendChild(ellipse); + return group; + } + + ]]> + </script> +</svg> diff --git a/pyramid/tests/test_asset.py b/pyramid/tests/test_asset.py index badb91d91..d3ebd5f7d 100644 --- a/pyramid/tests/test_asset.py +++ b/pyramid/tests/test_asset.py @@ -1,4 +1,7 @@ import unittest +import os + +here = os.path.abspath(os.path.dirname(__file__)) class Test_resolve_asset_spec(unittest.TestCase): def _callFUT(self, spec, package_name='__main__'): @@ -6,11 +9,8 @@ class Test_resolve_asset_spec(unittest.TestCase): return resolve_asset_spec(spec, package_name) def test_abspath(self): - import os - here = os.path.dirname(__file__) - path = os.path.abspath(here) - package_name, filename = self._callFUT(path, 'apackage') - self.assertEqual(filename, path) + package_name, filename = self._callFUT(here, 'apackage') + self.assertEqual(filename, here) self.assertEqual(package_name, None) def test_rel_spec(self): @@ -57,11 +57,8 @@ class Test_abspath_from_asset_spec(unittest.TestCase): self.assertEqual(result, '/abc') def test_pkgrelative(self): - import os - here = os.path.dirname(__file__) - path = os.path.abspath(here) result = self._callFUT('abc', 'pyramid.tests') - self.assertEqual(result, os.path.join(path, 'abc')) + self.assertEqual(result, os.path.join(here, 'abc')) class Test_asset_spec_from_abspath(unittest.TestCase): def _callFUT(self, abspath, package): @@ -74,20 +71,16 @@ class Test_asset_spec_from_abspath(unittest.TestCase): self.assertEqual(result, 'abspath') def test_abspath_startswith_package_path(self): - import os - abspath = os.path.join(os.path.dirname(__file__), 'fixtureapp') + abspath = os.path.join(here, 'fixtureapp') pkg = DummyPackage('pyramid.tests') pkg.__file__ = 'file' result = self._callFUT(abspath, pkg) self.assertEqual(result, 'pyramid:fixtureapp') def test_abspath_doesnt_startwith_package_path(self): - import os - abspath = os.path.dirname(__file__) pkg = DummyPackage('pyramid.tests') - result = self._callFUT(abspath, pkg) - self.assertEqual(result, abspath) - + result = self._callFUT(here, pkg) + self.assertEqual(result, here) class DummyPackage: def __init__(self, name): diff --git a/pyramid/tests/test_chameleon_zpt.py b/pyramid/tests/test_chameleon_zpt.py index 1a8e6767e..e7a1499e6 100644 --- a/pyramid/tests/test_chameleon_zpt.py +++ b/pyramid/tests/test_chameleon_zpt.py @@ -121,7 +121,7 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): instance = self._makeOne(minimal, lookup) self.assertRaises(ValueError, instance, None, {}) - @skip_on('java', 'pypy') + @skip_on('java') def test_implementation(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index ca1508295..3aa2c7810 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -228,6 +228,12 @@ class ConfiguratorTests(unittest.TestCase): request_iface=IRequest) self.assertTrue(view.__wraps__ is exceptionresponse_view) + def test_ctor_with_introspector(self): + introspector = DummyIntrospector() + config = self._makeOne(introspector=introspector) + self.assertEqual(config.introspector, introspector) + self.assertEqual(config.registry.introspector, introspector) + def test_with_package_module(self): from pyramid.tests.test_config import test_init import pyramid.tests @@ -637,6 +643,21 @@ pyramid.tests.test_config.dummy_include2""", [('pyramid.tests.test_config.dummy_tween_factory', dummy_tween_factory)]) + def test_introspector_decorator(self): + inst = self._makeOne() + default = inst.introspector + self.assertTrue(hasattr(default, 'add')) + self.assertEqual(inst.introspector, inst.registry.introspector) + introspector = DummyIntrospector() + inst.introspector = introspector + new = inst.introspector + self.assertTrue(new is introspector) + self.assertEqual(inst.introspector, inst.registry.introspector) + del inst.introspector + default = inst.introspector + self.assertFalse(default is new) + self.assertTrue(hasattr(default, 'add')) + def test_make_wsgi_app(self): import pyramid.config from pyramid.router import Router @@ -662,10 +683,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - after.actions[0][:3], - ('discrim', None, test_config), - ) + action = after.actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_python_callable(self): from pyramid.tests import test_config @@ -674,10 +695,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - actions[0][:3], - ('discrim', None, test_config), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_module_defaults_to_includeme(self): from pyramid.tests import test_config @@ -686,10 +707,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - actions[0][:3], - ('discrim', None, test_config), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_route_prefix(self): root_config = self._makeOne(autocommit=True) @@ -740,6 +761,15 @@ pyramid.tests.test_config.dummy_include2""", config = self._makeOne(autocommit=True) self.assertEqual(config.action('discrim', kw={'a':1}), None) + def test_action_autocommit_with_introspectables(self): + from pyramid.config.util import ActionInfo + config = self._makeOne(autocommit=True) + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 1) + self.assertEqual(intr.registered[0][0], config.introspector) + self.assertEqual(intr.registered[0][1].__class__, ActionInfo) + def test_action_branching_nonautocommit_with_config_info(self): config = self._makeOne(autocommit=False) config.info = 'abc' @@ -749,9 +779,15 @@ pyramid.tests.test_config.dummy_include2""", config.action('discrim', kw={'a':1}) self.assertEqual( state.actions, - [(('discrim', None, (), {'a': 1}, 0), - {'info': 'abc', 'includepath':()})] - ) + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0})]) def test_action_branching_nonautocommit_without_config_info(self): config = self._makeOne(autocommit=False) @@ -763,9 +799,27 @@ pyramid.tests.test_config.dummy_include2""", config.action('discrim', kw={'a':1}) self.assertEqual( state.actions, - [(('discrim', None, (), {'a': 1}, 0), - {'info': 'z', 'includepath':()})] - ) + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'z', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0})]) + + def test_action_branching_nonautocommit_with_introspectables(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual( + state.actions[0][1]['introspectables'], (intr,)) def test_scan_integration(self): from zope.interface import alsoProvides @@ -922,7 +976,7 @@ pyramid.tests.test_config.dummy_include2""", conflicts = e._conflicts.values() for conflict in conflicts: for confinst in conflict: - yield confinst[3] + yield confinst.src which = list(scanconflicts(why)) self.assertEqual(len(which), 4) self.assertTrue("@view_config(renderer='string')" in which) @@ -1313,10 +1367,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, test_config), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_extend_with_python_callable(self): from pyramid.tests import test_config @@ -1326,10 +1380,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, test_config), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_extend_same_name_doesnt_conflict(self): config = self.config @@ -1340,10 +1394,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, config.registry), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config.registry) def test_extend_action_method_successful(self): config = self.config @@ -1361,10 +1415,10 @@ class TestConfigurator_add_directive(unittest.TestCase): after = config2.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - after.actions[0][:3], - ('discrim', None, config2.package), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config2.package) class TestActionState(unittest.TestCase): def _makeOne(self): @@ -1380,42 +1434,120 @@ class TestActionState(unittest.TestCase): c = self._makeOne() c.actions = [] c.action(1, f, (1,), {'x':1}) - self.assertEqual(c.actions, [(1, f, (1,), {'x': 1})]) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0}]) c.action(None) - self.assertEqual(c.actions, [(1, f, (1,), {'x': 1}), (None, None)]) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0}, + + {'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0},]) def test_action_with_includepath(self): c = self._makeOne() c.actions = [] c.action(None, includepath=('abc',)) - self.assertEqual(c.actions, [(None, None, (), {}, ('abc',))]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('abc',), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0}]) def test_action_with_info(self): c = self._makeOne() c.action(None, info='abc') - self.assertEqual(c.actions, [(None, None, (), {}, (), 'abc')]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {}, + 'order': 0}]) def test_action_with_includepath_and_info(self): c = self._makeOne() c.action(None, includepath=('spec',), info='bleh') - self.assertEqual(c.actions, - [(None, None, (), {}, ('spec',), 'bleh')]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('spec',), + 'info': 'bleh', + 'introspectables': (), + 'kw': {}, + 'order': 0}]) def test_action_with_order(self): c = self._makeOne() c.actions = [] c.action(None, order=99999) - self.assertEqual(c.actions, [(None, None, (), {}, (), '', 99999)]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 99999}]) + + def test_action_with_introspectables(self): + c = self._makeOne() + c.actions = [] + intr = DummyIntrospectable() + c.action(None, introspectables=(intr,)) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + 'kw': {}, + 'order': 0}]) def test_processSpec(self): c = self._makeOne() self.assertTrue(c.processSpec('spec')) self.assertFalse(c.processSpec('spec')) - def test_execute_actions_simple(self): + def test_execute_actions_tuples(self): output = [] def f(*a, **k): - output.append(('f', a, k)) + output.append((a, k)) c = self._makeOne() c.actions = [ (1, f, (1,)), @@ -1424,7 +1556,57 @@ class TestActionState(unittest.TestCase): (None, None), ] c.execute_actions() - self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_dicts(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':1, 'callable':f, 'args':(11,), 'kw':{}, + 'includepath':('x',), 'order': 0, 'info':None, + 'introspectables':()}, + {'discriminator':2, 'callable':f, 'args':(2,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':None, 'callable':None, 'args':(), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_with_introspectables(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = DummyIntrospector() + c.execute_actions(introspector=introspector) + self.assertEqual(output, [((1,), {})]) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_with_introspectable_no_callable(self): + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':None, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = DummyIntrospector() + c.execute_actions(introspector=introspector) + self.assertEqual(intr.registered, [(introspector, None)]) def test_execute_actions_error(self): output = [] @@ -1447,7 +1629,7 @@ class Test_resolveConflicts(unittest.TestCase): from pyramid.config import resolveConflicts return resolveConflicts(actions) - def test_it_success(self): + def test_it_success_tuples(self): from pyramid.tests.test_config import dummyfactory as f result = self._callFUT([ (None, f), @@ -1458,12 +1640,115 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) - self.assertEqual(result, - [(None, f), - (1, f, (1,), {}, (), 'first'), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - (4, f, (4,), {}, ('y',), 'should be last')]) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0}, + + {'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0}, + + {'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0}, + + {'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + def test_it_success_dicts(self): + from pyramid.tests.test_config import dummyfactory as f + from pyramid.config import expand_action + result = self._callFUT([ + expand_action(None, f), + expand_action(1, f, (1,), {}, (), 'first'), + expand_action(1, f, (2,), {}, ('x',), 'second'), + expand_action(1, f, (3,), {}, ('y',), 'third'), + expand_action(4, f, (4,), {}, ('y',), 'should be last', 99999), + expand_action(3, f, (3,), {}, ('y',)), + expand_action(None, f, (5,), {}, ('y',)), + ]) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0}, + + {'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0}, + + {'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0}, + + {'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999} + ] + ) def test_it_conflict(self): from pyramid.tests.test_config import dummyfactory as f @@ -1479,6 +1764,81 @@ class Test_resolveConflicts(unittest.TestCase): ] ) + def test_it_with_actions_grouped_by_order(self): + from pyramid.tests.test_config import dummyfactory as f + from pyramid.config import expand_action + result = self._callFUT([ + expand_action(None, f), + expand_action(1, f, (1,), {}, (), 'third', 10), + expand_action(1, f, (2,), {}, ('x',), 'fourth', 10), + expand_action(1, f, (3,), {}, ('y',), 'fifth', 10), + expand_action(2, f, (1,), {}, (), 'sixth', 10), + expand_action(3, f, (1,), {}, (), 'seventh', 10), + expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), + expand_action(4, f, (3,), {}, (), 'first', 5), + expand_action(4, f, (5,), {}, ('y',), 'second', 5), + ]) + self.assertEqual(len(result), 6) + # resolved actions should be grouped by (order, i) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': (), + 'order': 5}, + + {'info': 'third', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 10}, + + {'info': 'sixth', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 2, + 'includepath': (), + 'order': 10}, + + {'info': 'seventh', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': (), + 'order': 10}, + + {'info': 'eighth', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 5, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + class TestGlobalRegistriesIntegration(unittest.TestCase): def setUp(self): from pyramid.config import global_registries @@ -1565,7 +1925,7 @@ def _conflictFunctions(e): conflicts = e._conflicts.values() for conflict in conflicts: for confinst in conflict: - yield confinst[2] + yield confinst.function class DummyActionState(object): autocommit = False @@ -1584,3 +1944,15 @@ class DummyZCMLContext(object): includepath = () info = '' +class DummyIntrospector(object): + def __init__(self): + self.intrs = [] + def add(self, intr): + self.intrs.append(intr) + +class DummyIntrospectable(object): + def __init__(self): + self.registered = [] + def register(self, introspector, action_info): + self.registered.append((introspector, action_info)) + diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index 1646561cd..140a4aa73 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -52,7 +52,8 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): def test_add_route_discriminator(self): config = self._makeOne() config.add_route('name', 'path') - self.assertEqual(config.action_state.actions[-1][0], ('route', 'name')) + self.assertEqual(config.action_state.actions[-1]['discriminator'], + ('route', 'name')) def test_add_route_with_factory(self): config = self._makeOne(autocommit=True) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index bc7cf0a82..1180e7e29 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -312,6 +312,36 @@ class Test__make_predicates(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) +class TestActionInfo(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.util import ActionInfo + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') + + def test___str__(self): + inst = self._makeOne('filename', 0, 'function', ' linerepr ') + self.assertEqual(str(inst), + "Line 0 of file filename:\n linerepr ") + class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 0813eecdb..30a30a1e8 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1362,6 +1362,47 @@ class TestViewsConfigurationMixin(unittest.TestCase): request = self._makeRequest(config) self.assertEqual(view(None, request), 'OK') + def test_add_view_with_mapper(self): + from pyramid.renderers import null_renderer + class Mapper(object): + def __init__(self, **kw): + self.__class__.kw = kw + def __call__(self, view): + return view + config = self._makeOne(autocommit=True) + def view(context, request): return 'OK' + config.add_view(view=view, mapper=Mapper, renderer=null_renderer) + view = self._getViewCallable(config) + self.assertEqual(view(None, None), 'OK') + self.assertEqual(Mapper.kw['mapper'], Mapper) + + def test_add_view_with_view_defaults(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view( + view=view, + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): @@ -1844,9 +1885,23 @@ class TestMultiView(unittest.TestCase): mv.add('view', 100, phash='abc') self.assertEqual(mv.views, [(100, 'view', 'abc')]) mv.add('view', 100, phash='def') - self.assertEqual(mv.views, [(100, 'view', 'abc'), (100, 'view', 'def')]) + self.assertEqual(mv.views, [(100, 'view', 'abc'), + (100, 'view', 'def')]) mv.add('view', 100, phash='abc') - self.assertEqual(mv.views, [(100, 'view', 'abc'), (100, 'view', 'def')]) + self.assertEqual(mv.views, [(100, 'view', 'abc'), + (100, 'view', 'def')]) + + def test_multiple_with_functions_as_views(self): + # this failed on py3 at one point, because functions aren't orderable + # and we were sorting the views via a plain sort() rather than + # sort(key=itemgetter(0)). + def view1(request): pass + def view2(request): pass + mv = self._makeOne() + mv.add(view1, 100, None) + self.assertEqual(mv.views, [(100, view1, None)]) + mv.add(view2, 100, None) + self.assertEqual(mv.views, [(100, view1, None), (100, view2, None)]) def test_get_views_request_has_no_accept(self): request = DummyRequest() @@ -3348,6 +3403,27 @@ class TestStaticURLInfo(unittest.TestCase): result = inst.generate('package:path/abc', request, a=1) self.assertEqual(result, 'url') + def test_generate_url_quoted_local(self): + inst = self._makeOne() + registrations = [(None, 'package:path/', '__viewname/')] + inst._get_registrations = lambda *x: registrations + def route_url(n, **kw): + self.assertEqual(n, '__viewname/') + self.assertEqual(kw, {'subpath':'abc%20def', 'a':1}) + return 'url' + request = self._makeRequest() + request.route_url = route_url + result = inst.generate('package:path/abc def', request, a=1) + self.assertEqual(result, 'url') + + def test_generate_url_quoted_remote(self): + inst = self._makeOne() + registrations = [('http://example.com/', 'package:path/', None)] + inst._get_registrations = lambda *x: registrations + request = self._makeRequest() + result = inst.generate('package:path/abc def', request, a=1) + self.assertEqual(result, 'http://example.com/abc%20def') + def test_add_already_exists(self): inst = self._makeOne() config = self._makeConfig( @@ -3512,9 +3588,12 @@ class DummyConfig: self.view_args = args self.view_kw = kw - def action(self, discriminator, callable): + def action(self, discriminator, callable, introspectables=()): callable() + def introspectable(self, *arg): + return {} + from zope.interface import implementer from pyramid.interfaces import IMultiView @implementer(IMultiView) diff --git a/pyramid/tests/test_docs.py b/pyramid/tests/test_docs.py index eba95b210..0735a494a 100644 --- a/pyramid/tests/test_docs.py +++ b/pyramid/tests/test_docs.py @@ -1,5 +1,4 @@ import unittest -from pyramid.compat import print_ if 0: # no released version of manuel actually works with :lineno: @@ -32,5 +31,5 @@ if 0: if filename.endswith('.rst'): docs.append(os.path.join(root, filename)) - print_(path) + print(path) return manuel.testing.TestSuite(m, *docs) diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index 108a5d2d9..4b58a129c 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -122,11 +122,10 @@ class ContextFoundEventTests(unittest.TestCase): class TestSubscriber(unittest.TestCase): def setUp(self): - registry = DummyRegistry() - self.config = testing.setUp(registry=registry) + self.config = testing.setUp() def tearDown(self): - self.config.end() + testing.tearDown() def _makeOne(self, *ifaces): from pyramid.events import subscriber diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 927d27733..84485fadc 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -294,6 +294,12 @@ class Test_HTTPMove(unittest.TestCase): from pyramid.httpexceptions import _HTTPMove return _HTTPMove(*arg, **kw) + def test_it_location_none_valueerrors(self): + # Constructing a HTTPMove instance with location=None should + # throw a ValueError from __init__ so that a more-confusing + # exception won't be thrown later from .prepare(environ) + self.assertRaises(ValueError, self._makeOne, location=None) + def test_it_location_not_passed(self): exc = self._makeOne() self.assertEqual(exc.location, '') diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 981a5056e..4c444facf 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -22,14 +22,20 @@ class Test_renderer_factory(Base, unittest.TestCase): return renderer_factory(info) def test_no_directories(self): - from pyramid.exceptions import ConfigurationError + from pyramid.mako_templating import IMakoLookup info = DummyRendererInfo({ - 'name':'helloworld.mak', + 'name':'pyramid.tests:fixtures/helloworld.mak', 'package':None, 'registry':self.config.registry, 'settings':{}, }) - self.assertRaises(ConfigurationError, self._callFUT, info) + renderer = self._callFUT(info) + lookup = self.config.registry.getUtility(IMakoLookup) + self.assertEqual(lookup.directories, []) + self.assertEqual(lookup.filesystem_checks, False) + self.assertEqual(renderer.path, + 'pyramid.tests:fixtures/helloworld.mak') + self.assertEqual(renderer.lookup, lookup) def test_no_lookup(self): from pyramid.mako_templating import IMakoLookup diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index d23c156c2..5901c0416 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -1,6 +1,6 @@ import unittest -class TestGetApp(unittest.TestCase): +class Test_get_app(unittest.TestCase): def _callFUT(self, config_file, section_name, loadapp): from pyramid.paster import get_app return get_app(config_file, section_name, loadapp) @@ -8,7 +8,7 @@ class TestGetApp(unittest.TestCase): def test_it(self): import os app = DummyApp() - loadapp = DummyLoadApp(app) + loadapp = DummyLoadWSGI(app) result = self._callFUT('/foo/bar/myapp.ini', 'myapp', loadapp) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'myapp') @@ -18,7 +18,7 @@ class TestGetApp(unittest.TestCase): def test_it_with_hash(self): import os app = DummyApp() - loadapp = DummyLoadApp(app) + loadapp = DummyLoadWSGI(app) result = self._callFUT('/foo/bar/myapp.ini#myapp', None, loadapp) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'myapp') @@ -28,14 +28,64 @@ class TestGetApp(unittest.TestCase): def test_it_with_hash_and_name_override(self): import os app = DummyApp() - loadapp = DummyLoadApp(app) + loadapp = DummyLoadWSGI(app) result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', loadapp) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'yourapp') self.assertEqual(loadapp.relative_to, os.getcwd()) self.assertEqual(result, app) -class TestBootstrap(unittest.TestCase): +class Test_get_appsettings(unittest.TestCase): + def _callFUT(self, config_file, section_name, appconfig): + from pyramid.paster import get_appsettings + return get_appsettings(config_file, section_name, appconfig) + + def test_it(self): + import os + values = {'a':1} + appconfig = DummyLoadWSGI(values) + result = self._callFUT('/foo/bar/myapp.ini', 'myapp', appconfig) + self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') + self.assertEqual(appconfig.section_name, 'myapp') + self.assertEqual(appconfig.relative_to, os.getcwd()) + self.assertEqual(result, values) + + def test_it_with_hash(self): + import os + values = {'a':1} + appconfig = DummyLoadWSGI(values) + result = self._callFUT('/foo/bar/myapp.ini#myapp', None, appconfig) + self.assertEqual(appconfig.config_name, 'config:/foo/bar/myapp.ini') + self.assertEqual(appconfig.section_name, 'myapp') + self.assertEqual(appconfig.relative_to, os.getcwd()) + self.assertEqual(result, values) + + def test_it_with_hash_and_name_override(self): + import os + values = {'a':1} + appconfig = DummyLoadWSGI(values) + result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', 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) + +class Test_setup_logging(unittest.TestCase): + def _callFUT(self, config_file): + from pyramid.paster import setup_logging + dummy_cp = DummyConfigParserModule + return setup_logging(config_file, self.fileConfig, dummy_cp) + + def test_it(self): + config_file, dict = self._callFUT('/abc') + self.assertEqual(config_file, '/abc') + self.assertEqual(dict['__file__'], '/abc') + self.assertEqual(dict['here'], '/') + + def fileConfig(self, config_file, dict): + return config_file, dict + +class Test_bootstrap(unittest.TestCase): def _callFUT(self, config_uri, request=None): from pyramid.paster import bootstrap return bootstrap(config_uri, request) @@ -82,15 +132,15 @@ class DummyRegistry(object): dummy_registry = DummyRegistry() -class DummyLoadApp: - def __init__(self, app): - self.app = app +class DummyLoadWSGI: + def __init__(self, result): + self.result = result def __call__(self, config_name, name=None, relative_to=None): self.config_name = config_name self.section_name = name self.relative_to = relative_to - return self.app + return self.result class DummyApp: def __init__(self): @@ -103,3 +153,15 @@ class DummyRequest: self.environ = environ self.matchdict = {} +class DummyConfigParser(object): + def read(self, x): + pass + + def has_section(self, name): + return True + +class DummyConfigParserModule(object): + ConfigParser = DummyConfigParser + + + diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index c2261d223..304afad7c 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -1,4 +1,8 @@ import unittest +import os +from pyramid.compat import PY3 + +here = os.path.abspath(os.path.dirname(__file__)) class TestCallerPath(unittest.TestCase): def tearDown(self): @@ -16,7 +20,6 @@ class TestCallerPath(unittest.TestCase): def test_pkgrelative(self): import os - here = os.path.abspath(os.path.dirname(__file__)) result = self._callFUT('a/b/c') self.assertEqual(result, os.path.join(here, 'a/b/c')) @@ -29,7 +32,6 @@ class TestCallerPath(unittest.TestCase): def test_memoization_success(self): import os - here = os.path.abspath(os.path.dirname(__file__)) from pyramid.tests import test_path result = self._callFUT('a/b/c') self.assertEqual(result, os.path.join(here, 'a/b/c')) @@ -162,7 +164,388 @@ class TestPackageName(unittest.TestCase): def test_it_None(self): result = self._callFUT(None) self.assertEqual(result, '__main__') - + + def test_it_main(self): + import __main__ + result = self._callFUT(__main__) + self.assertEqual(result, '__main__') + +class TestResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import Resolver + return Resolver + + def _makeOne(self, package): + return self._getTargetClass()(package) + + def test_get_package_caller_package(self): + import pyramid.tests + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package(), + pyramid.tests) + + def test_get_package_name_caller_package(self): + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package_name(), + 'pyramid.tests') + + def test_get_package_string(self): + import pyramid.tests + self.assertEqual(self._makeOne('pyramid.tests').get_package(), + pyramid.tests) + + def test_get_package_name_string(self): + self.assertEqual(self._makeOne('pyramid.tests').get_package_name(), + 'pyramid.tests') + +class TestAssetResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import AssetResolver + return AssetResolver + + def _makeOne(self, package='pyramid.tests'): + return self._getTargetClass()(package) + + def test_ctor_as_package(self): + import sys + tests = sys.modules['pyramid.tests'] + inst = self._makeOne(tests) + self.assertEqual(inst.package, tests) + + def test_ctor_as_str(self): + import sys + tests = sys.modules['pyramid.tests'] + inst = self._makeOne('pyramid.tests') + self.assertEqual(inst.package, tests) + + def test_resolve_abspath(self): + from pyramid.path import FSAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve(os.path.join(here, 'test_asset.py')) + self.assertEqual(r.__class__, FSAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_absspec(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve('pyramid.tests:test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_relspec_with_pkg(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne('pyramid.tests') + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_relspec_no_package(self): + inst = self._makeOne(None) + self.assertRaises(ValueError, inst.resolve, 'test_asset.py') + + def test_resolve_relspec_caller_package(self): + from pyramid.path import PkgResourcesAssetDescriptor + from pyramid.path import CALLER_PACKAGE + inst = self._makeOne(CALLER_PACKAGE) + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + +class TestPkgResourcesAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import PkgResourcesAssetDescriptor + return PkgResourcesAssetDescriptor + + def _makeOne(self, pkg='pyramid.tests', path='test_asset.py'): + return self._getTargetClass()(pkg, path) + + def test_class_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + klass = self._getTargetClass() + verifyClass(IAssetDescriptor, klass) + + def test_instance_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + inst = self._makeOne() + verifyObject(IAssetDescriptor, inst) + + def test_absspec(self): + inst = self._makeOne() + self.assertEqual(inst.absspec(), 'pyramid.tests:test_asset.py') + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_stream = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.stream(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_isdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_isdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.isdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_listdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_listdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.listdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_exists(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_exists = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.exists(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + +class TestFSAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import FSAssetDescriptor + return FSAssetDescriptor + + def _makeOne(self, path=os.path.join(here, 'test_asset.py')): + return self._getTargetClass()(path) + + def test_class_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + klass = self._getTargetClass() + verifyClass(IAssetDescriptor, klass) + + def test_instance_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + inst = self._makeOne() + verifyObject(IAssetDescriptor, inst) + + def test_absspec(self): + inst = self._makeOne() + self.assertRaises(NotImplementedError, inst.absspec) + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + val = inst.stream().read() + self.assertTrue(b'asset' in val) + + def test_isdir_False(self): + inst = self._makeOne() + self.assertFalse(inst.isdir()) + + def test_isdir_True(self): + inst = self._makeOne(here) + self.assertTrue(inst.isdir()) + + def test_listdir(self): + inst = self._makeOne(here) + self.assertTrue(inst.listdir()) + + def test_exists(self): + inst = self._makeOne() + self.assertTrue(inst.exists()) + +class TestDottedNameResolver(unittest.TestCase): + def _makeOne(self, package=None): + from pyramid.path import DottedNameResolver + return DottedNameResolver(package) + + def config_exc(self, func, *arg, **kw): + try: + func(*arg, **kw) + except ValueError as e: + return e + else: + raise AssertionError('Invalid not raised') # pragma: no cover + + def test_zope_dottedname_style_resolve_builtin(self): + typ = self._makeOne() + if PY3: # pragma: no cover + result = typ._zope_dottedname_style('builtins.str', None) + else: + result = typ._zope_dottedname_style('__builtin__.str', None) + self.assertEqual(result, str) + + def test_zope_dottedname_style_resolve_absolute(self): + typ = self._makeOne() + result = typ._zope_dottedname_style( + 'pyramid.tests.test_path.TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test_zope_dottedname_style_irrresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ._zope_dottedname_style, + 'pyramid.test_path.nonexisting_name', None) + + def test__zope_dottedname_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne() + result = typ._zope_dottedname_style( + '.test_path.TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__zope_dottedname_style_resolve_relative_leading_dots(self): + import pyramid.tests.test_configuration + typ = self._makeOne() + result = typ._zope_dottedname_style( + '..tests.test_path.TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__zope_dottedname_style_resolve_relative_is_dot(self): + import pyramid.tests + typ = self._makeOne() + result = typ._zope_dottedname_style('.', pyramid.tests) + self.assertEqual(result, pyramid.tests) + + def test__zope_dottedname_style_irresolveable_relative_is_dot(self): + typ = self._makeOne() + e = self.config_exc(typ._zope_dottedname_style, '.', None) + self.assertEqual( + e.args[0], + "relative name '.' irresolveable without package") + + def test_zope_dottedname_style_resolve_relative_nocurrentpackage(self): + typ = self._makeOne() + e = self.config_exc(typ._zope_dottedname_style, '.whatever', None) + self.assertEqual( + e.args[0], + "relative name '.whatever' irresolveable without package") + + def test_zope_dottedname_style_irrresolveable_relative(self): + import pyramid.tests + typ = self._makeOne() + self.assertRaises(ImportError, typ._zope_dottedname_style, + '.notexisting', pyramid.tests) + + def test__zope_dottedname_style_resolveable_relative(self): + import pyramid + typ = self._makeOne() + result = typ._zope_dottedname_style('.tests', pyramid) + from pyramid import tests + self.assertEqual(result, tests) + + def test__zope_dottedname_style_irresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises( + ImportError, + typ._zope_dottedname_style, 'pyramid.fudge.bar', None) + + def test__zope_dottedname_style_resolveable_absolute(self): + typ = self._makeOne() + result = typ._zope_dottedname_style( + 'pyramid.tests.test_path.TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_resolve_absolute(self): + typ = self._makeOne() + result = typ._pkg_resources_style( + 'pyramid.tests.test_path:TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_irrresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ._pkg_resources_style, + 'pyramid.tests:nonexisting', None) + + def test__pkg_resources_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne() + result = typ._pkg_resources_style( + '.test_path:TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_resolve_relative_is_dot(self): + import pyramid.tests + typ = self._makeOne() + result = typ._pkg_resources_style('.', pyramid.tests) + self.assertEqual(result, pyramid.tests) + + def test__pkg_resources_style_resolve_relative_nocurrentpackage(self): + typ = self._makeOne() + self.assertRaises(ValueError, typ._pkg_resources_style, + '.whatever', None) + + def test__pkg_resources_style_irrresolveable_relative(self): + import pyramid + typ = self._makeOne() + self.assertRaises(ImportError, typ._pkg_resources_style, + ':notexisting', pyramid) + + def test_resolve_not_a_string(self): + typ = self._makeOne() + e = self.config_exc(typ.resolve, None) + self.assertEqual(e.args[0], 'None is not a string') + + def test_resolve_using_pkgresources_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_using_zope_dottedname_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_missing_raises(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ.resolve, 'cant.be.found') + + def test_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.resolve('.test_path.TestDottedNameResolver'), + self.__class__) + + def test_maybe_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.maybe_resolve('.test_path.TestDottedNameResolver'), + self.__class__) + + def test_ctor_string_module_resolveable(self): + import pyramid.tests + typ = self._makeOne('pyramid.tests.test_path') + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_string_package_resolveable(self): + import pyramid.tests + typ = self._makeOne('pyramid.tests') + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_string_irresolveable(self): + self.assertRaises(ValueError, self._makeOne, 'cant.be.found') + + def test_ctor_module(self): + import pyramid.tests + import pyramid.tests.test_path + typ = self._makeOne(pyramid.tests.test_path) + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_package(self): + import pyramid.tests + typ = self._makeOne(pyramid.tests) + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_None(self): + typ = self._makeOne(None) + self.assertEqual(typ.package, None) + +class DummyPkgResource(object): + pass + class DummyPackageOrModule: def __init__(self, real_package_or_module, raise_exc=None): self.__dict__['raise_exc'] = raise_exc @@ -176,9 +559,3 @@ class DummyPackageOrModule: if self.raise_exc is not None: raise self.raise_exc self.__dict__[key] = val - - - - - - diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index c3104bd31..29803346a 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -42,11 +42,362 @@ class TestRegistry(unittest.TestCase): registry.settings = 'foo' self.assertEqual(registry._settings, 'foo') +class TestIntrospector(unittest.TestCase): + def _getTargetClass(slf): + from pyramid.registry import Introspector + return Introspector + + def _makeOne(self): + return self._getTargetClass()() + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospector + verifyClass(IIntrospector, self._getTargetClass()) + verifyObject(IIntrospector, self._makeOne()) + + def test_add(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(intr.order, 0) + category = {'discriminator':intr, 'discriminator_hash':intr} + self.assertEqual(inst._categories, {'category':category}) + + def test_get_success(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator'), intr) + + def test_get_success_byhash(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator_hash'), intr) + + def test_get_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'wontexist', 'foo'), 'foo') + + def test_get_category(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr2) + inst.add(intr) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual(inst.get_category('category'), expected) + + def test_get_category_returns_default_on_miss(self): + inst = self._makeOne() + self.assertEqual(inst.get_category('category', '123'), '123') + + def test_get_category_with_sortkey(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual( + inst.get_category('category', sort_key=operator.attrgetter('foo')), + expected) + + def test_categorized(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [('category', [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ])] + self.assertEqual( + inst.categorized(sort_key=operator.attrgetter('foo')), expected) + + def test_categories(self): + inst = self._makeOne() + inst._categories['a'] = 1 + inst._categories['b'] = 2 + self.assertEqual(list(inst.categories()), ['a', 'b']) + + def test_remove(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.remove('category', 'discriminator') + self.assertEqual(inst._categories, + {'category': + {}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs.get(intr), None) + self.assertEqual(inst._refs[intr2], []) + + def test_remove_fail(self): + inst = self._makeOne() + self.assertEqual(inst.remove('a', 'b'), None) + + def test_relate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], [intr2]) + self.assertEqual(inst._refs[intr2], [intr]) + + def test_relate_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertRaises( + KeyError, + inst.relate, + ('category', 'discriminator'), + ('category2', 'discriminator2') + ) + + def test_unrelate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.unrelate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], []) + self.assertEqual(inst._refs[intr2], []) + + def test_related(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst.related(intr), [intr2]) + + def test_related_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + del inst._categories['category'] + self.assertRaises(KeyError, inst.related, intr) + +class Test_noop_introspector(unittest.TestCase): + def _makeOne(self): + from pyramid.registry import noop_introspector + return noop_introspector + + def test_conformance(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospector + verifyObject(IIntrospector, self._makeOne()) + + def test_add(self): + inst = self._makeOne() + self.assertEqual(inst.add('a'), None) + + def test_get(self): + inst = self._makeOne() + self.assertEqual(inst.get('category', 'd', default='123'), '123') + + def test_get_category(self): + inst = self._makeOne() + self.assertEqual(inst.get_category('category', default='123'), '123') + + def test_categorized(self): + inst = self._makeOne() + self.assertEqual(inst.categorized(), []) + + def test_categories(self): + inst = self._makeOne() + self.assertEqual(inst.categories(), []) + + def test_remove(self): + inst = self._makeOne() + self.assertEqual(inst.remove('cat', 'discrim'), None) + + def test_relate(self): + inst = self._makeOne() + self.assertEqual(inst.relate(), None) + + def test_unrelate(self): + inst = self._makeOne() + self.assertEqual(inst.unrelate(), None) + + def test_related(self): + inst = self._makeOne() + self.assertEqual(inst.related('a'), []) + +class TestIntrospectable(unittest.TestCase): + def _getTargetClass(slf): + from pyramid.registry import Introspectable + return Introspectable + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def _makeOnePopulated(self): + return self._makeOne('category', 'discrim', 'title', 'type') + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospectable + verifyClass(IIntrospectable, self._getTargetClass()) + verifyObject(IIntrospectable, self._makeOnePopulated()) + + def test_relate(self): + inst = self._makeOnePopulated() + inst.relate('a', 'b') + self.assertEqual(inst._relations, [(True, 'a', 'b')]) + + def test_unrelate(self): + inst = self._makeOnePopulated() + inst.unrelate('a', 'b') + self.assertEqual(inst._relations, [(False, 'a', 'b')]) + + def test_discriminator_hash(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.discriminator_hash, hash(inst.discriminator)) + + def test___hash__(self): + inst = self._makeOnePopulated() + self.assertEqual(hash(inst), + hash((inst.category_name,) + (inst.discriminator,))) + + def test___repr__(self): + inst = self._makeOnePopulated() + self.assertEqual( + repr(inst), + "<Introspectable category 'category', discriminator 'discrim'>") + + def test___nonzero__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__nonzero__(), True) + + def test___bool__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__bool__(), True) + + def test_register(self): + introspector = DummyIntrospector() + action_info = object() + inst = self._makeOnePopulated() + inst._relations.append((True, 'category1', 'discrim1')) + inst._relations.append((False, 'category2', 'discrim2')) + inst.register(introspector, action_info) + self.assertEqual(inst.action_info, action_info) + self.assertEqual(introspector.intrs, [inst]) + self.assertEqual(introspector.relations, + [(('category', 'discrim'), ('category1', 'discrim1'))]) + self.assertEqual(introspector.unrelations, + [(('category', 'discrim'), ('category2', 'discrim2'))]) + +class DummyIntrospector(object): + def __init__(self): + self.intrs = [] + self.relations = [] + self.unrelations = [] + + def add(self, intr): + self.intrs.append(intr) + + def relate(self, *pairs): + self.relations.append(pairs) + + def unrelate(self, *pairs): + self.unrelations.append(pairs) + class DummyModule: __path__ = "foo" __name__ = "dummy" __file__ = '' +class DummyIntrospectable(object): + category_name = 'category' + discriminator = 'discriminator' + title = 'title' + type_name = 'type' + order = None + action_info = None + discriminator_hash = 'discriminator_hash' + + def __hash__(self): + return hash((self.category_name,) + (self.discriminator,)) + + from zope.interface import Interface from zope.interface import implementer class IDummyEvent(Interface): diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 1054dcb1c..dbdfb06b3 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -340,64 +340,6 @@ class TestChameleonRendererLookup(unittest.TestCase): self.assertNotEqual(reg.queryUtility(ITemplateRenderer, name=spec), None) -class TestRendererFromName(unittest.TestCase): - def setUp(self): - from zope.deprecation import __show__ - __show__.off() - self.config = cleanUp() - - def tearDown(self): - cleanUp() - from zope.deprecation import __show__ - __show__.on() - - def _callFUT(self, path, package=None): - from pyramid.renderers import renderer_from_name - return renderer_from_name(path, package) - - def test_it(self): - registry = self.config.registry - settings = {} - registry.settings = settings - from pyramid.interfaces import IRendererFactory - import os - here = os.path.dirname(os.path.abspath(__file__)) - fixture = os.path.join(here, 'fixtures/minimal.pt') - def factory(info, **kw): - return info - self.config.registry.registerUtility( - factory, IRendererFactory, name='.pt') - result = self._callFUT(fixture) - self.assertEqual(result.registry, registry) - self.assertEqual(result.type, '.pt') - self.assertEqual(result.package, None) - self.assertEqual(result.name, fixture) - self.assertEqual(result.settings, settings) - - def test_it_with_package(self): - import pyramid - registry = self.config.registry - settings = {} - registry.settings = settings - from pyramid.interfaces import IRendererFactory - import os - here = os.path.dirname(os.path.abspath(__file__)) - fixture = os.path.join(here, 'fixtures/minimal.pt') - def factory(info, **kw): - return info - self.config.registry.registerUtility( - factory, IRendererFactory, name='.pt') - result = self._callFUT(fixture, pyramid) - self.assertEqual(result.registry, registry) - self.assertEqual(result.type, '.pt') - self.assertEqual(result.package, pyramid) - self.assertEqual(result.name, fixture) - self.assertEqual(result.settings, settings) - - def test_it_no_renderer(self): - self.assertRaises(ValueError, self._callFUT, 'foo') - - class Test_json_renderer_factory(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -719,6 +661,49 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(cloned_helper.registry, 'registry2') self.assertFalse(helper is cloned_helper) + def test_renderer_absolute_file(self): + registry = self.config.registry + settings = {} + registry.settings = settings + from pyramid.interfaces import IRendererFactory + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.pt') + def factory(info, **kw): + return info + self.config.registry.registerUtility( + factory, IRendererFactory, name='.pt') + result = self._makeOne(fixture).renderer + self.assertEqual(result.registry, registry) + self.assertEqual(result.type, '.pt') + self.assertEqual(result.package, None) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_renderer_with_package(self): + import pyramid + registry = self.config.registry + settings = {} + registry.settings = settings + from pyramid.interfaces import IRendererFactory + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.pt') + def factory(info, **kw): + return info + self.config.registry.registerUtility( + factory, IRendererFactory, name='.pt') + result = self._makeOne(fixture, pyramid).renderer + self.assertEqual(result.registry, registry) + self.assertEqual(result.type, '.pt') + self.assertEqual(result.package, pyramid) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_renderer_missing(self): + inst = self._makeOne('foo') + self.assertRaises(ValueError, getattr, inst, 'renderer') + class TestNullRendererHelper(unittest.TestCase): def setUp(self): self.config = cleanUp() diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 9d498e12f..546f670c0 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -249,7 +249,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(request.json_body, {'a':1}) def test_json_body_alternate_charset(self): - from pyramid.compat import json + import json request = self._makeOne({'REQUEST_METHOD':'POST'}) inp = text_( b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf', diff --git a/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile b/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile diff --git a/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl b/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 114de266f..01f9b19ff 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import unittest import os import pkg_resources @@ -63,7 +64,341 @@ class Test_copy_dir(unittest.TestCase): scontent = f.read() self.assertEqual(scontent, tcontent) + def test_content_is_same_message(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('%s already exists (same content)' % \ + os.path.join(self.dirname, 'mypackage', '__init__.py') in result) + + def test_direxists_message(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + # if not os.path.exists(self.dirname): + # os.mkdir(self.dirname) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('Directory %s exists' % self.dirname in result, result) + + def test_overwrite_false(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + target = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(target, 'w') as f: + f.write('These are not the words you are looking for.') + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + + def test_detect_SkipTemplate(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + def dummy_template_renderer(*args, **kwargs): + from pyramid.scaffolds.copydir import SkipTemplate + raise SkipTemplate + self._callFUT(source, + self.dirname, + vars, + 1, False, + template_renderer=dummy_template_renderer) + + def test_query_interactive(self): + from pyramid.scaffolds import copydir + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + target = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(target, 'w') as f: + f.write('These are not the words you are looking for.') + # We need query_interactive to return False in order to force + # execution of a branch + original_code_object = copydir.query_interactive + copydir.query_interactive = lambda *args, **kwargs: False + self._callFUT(source, + self.dirname, + vars, + 1, False, + interactive=True, + overwrite=False, + template_renderer=dummy_template_renderer) + copydir.query_interactive = original_code_object + +class Test_raise_SkipTemplate(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import skip_template + return skip_template(*arg, **kw) + + def test_raise_SkipTemplate(self): + from pyramid.scaffolds.copydir import SkipTemplate + self.assertRaises(SkipTemplate, + self._callFUT, True, "exc-message") + +class Test_makedirs(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import makedirs + return makedirs(*arg, **kw) + + def test_makedirs_parent_dir(self): + import shutil + target = "/tmp/nonexistent_dir/nonexistent_subdir" + self._callFUT(target, 2, None) + shutil.rmtree("/tmp/nonexistent_dir") + +class Test_support_functions(unittest.TestCase): + + def _call_html_quote(self, *arg, **kw): + from pyramid.scaffolds.copydir import html_quote + return html_quote(*arg, **kw) + + def _call_url_quote(self, *arg, **kw): + from pyramid.scaffolds.copydir import url_quote + return url_quote(*arg, **kw) + + def _call_test(self, *arg, **kw): + from pyramid.scaffolds.copydir import test + return test(*arg, **kw) + + def test_html_quote(self): + import string + s = None + self.assertEqual(self._call_html_quote(s), '') + s = string.ascii_letters + self.assertEqual(self._call_html_quote(s), s) + s = "Λεμεσός" + self.assertEqual(self._call_url_quote(s), + "%CE%9B%CE%B5%CE%BC%CE%B5%CF%83%CF%8C%CF%82") + + def test_url_quote(self): + import string + s = None + self.assertEqual(self._call_url_quote(s), '') + s = string.ascii_letters + self.assertEqual(self._call_url_quote(s), s) + s = "Λεμεσός" + self.assertEqual(self._call_url_quote(s), + "%CE%9B%CE%B5%CE%BC%CE%B5%CF%83%CF%8C%CF%82") + + def test_test(self): + conf = True + true_cond = "faked" + self.assertEqual(self._call_test( + conf, true_cond, false_cond=None), "faked") + conf = False + self.assertEqual(self._call_test( + conf, true_cond, false_cond="alsofaked"), "alsofaked") + + +class Test_should_skip_file(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import should_skip_file + return should_skip_file(*arg, **kw) + + def test_should_skip_dot_hidden_file(self): + self.assertEqual( + self._callFUT('.a_filename'), + 'Skipping hidden file %(filename)s') + + def test_should_skip_backup_file(self): + for name in ('a_filename~', 'a_filename.bak'): + self.assertEqual( + self._callFUT(name), + 'Skipping backup file %(filename)s') + + def test_should_skip_bytecompiled_file(self): + for name in ('afilename.pyc', 'afilename.pyo'): + extension = os.path.splitext(name)[1] + self.assertEqual( + self._callFUT(name), + 'Skipping %s file ' % extension + '%(filename)s') + + def test_should_skip_jython_class_file(self): + self.assertEqual( + self._callFUT('afilename$py.class'), + 'Skipping $py.class file %(filename)s') + + def test_should_skip_version_control_directory(self): + for name in ('CVS', '_darcs'): + self.assertEqual( + self._callFUT(name), + 'Skipping version control directory %(filename)s') + + def test_valid_file_is_not_skipped(self): + self.assertEqual( + self._callFUT('a_filename'), None) + +class RawInputMockObject( object ): + count = 0 + def __init__( self, fake_input ): + self.input= fake_input + self.count = 0 + def __call__( self, prompt ): + # Don't cycle endlessly. + self.count += 1 + if self.count > 1: + return 'y' + else: + return self.input + +class Test_query_interactive(unittest.TestCase): + + def setUp(self): + import tempfile + from pyramid.compat import NativeIO + self.dirname = tempfile.mkdtemp() + self.out = NativeIO() + self.fixturetuple = ('pyramid.tests.test_scaffolds', + 'fixture_scaffold') + self.src_content = """\ +These are not the droids +that you are looking for.""" + self.dest_content = """\ +These are the droids for +whom you are looking; +now you have found them.""" + self.src_fn = os.path.join(self.dirname, 'mypackage', '__init__.py') + self.dest_fn = os.path.join(self.dirname, 'mypackage', '__init__.py') + # query_interactive is only normally executed when the destination + # is discovered to be already occupied by existing files, so ... + # create the required occupancy. + from pyramid.scaffolds.copydir import copy_dir + copy_dir(self.fixturetuple, + self.dirname, + {'package':'mypackage'}, + 0, False, + template_renderer=dummy_template_renderer) + + def tearDown(self): + import shutil + shutil.rmtree(self.dirname, ignore_errors=True) + self.out.close() + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import query_interactive + return query_interactive(*arg, **kw) + + def test_query_interactive_0y(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("y") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + self.assertTrue("Replace" in self.out.getvalue()) + + def test_query_interactive_1n(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("n") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, + '\n'.join(self.dest_content.split('\n')[:-1]), + simulate=False, + out_=self.out) + self.assertTrue("Replace" in self.out.getvalue()) + + def test_query_interactive_2b(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("b") + with open(os.path.join( + self.dirname, 'mypackage', '__init__.py.bak'), 'w') as fp: + fp.write("") + fp.close() + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + self.assertTrue("Backing up" in self.out.getvalue()) + + def test_query_interactive_3d(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("d") + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("3d", output) + # self.assertTrue("@@" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_4dc(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("dc") + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise, the test could be made + # more usefully precise... + # print("4dc", output) + # self.assertTrue("***" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_5allbad(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("all z") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("5allbad", output) + # self.assertTrue("Responses" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_6all(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("all b") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("6all", output) + # self.assertTrue("Responses" in output, output) + self.assertTrue("Replace" in output) + def dummy_template_renderer(content, v, filename=None): return content - diff --git a/pyramid/tests/test_scaffolds/test_init.py b/pyramid/tests/test_scaffolds/test_init.py index 6b038914a..4988e66ff 100644 --- a/pyramid/tests/test_scaffolds/test_init.py +++ b/pyramid/tests/test_scaffolds/test_init.py @@ -11,6 +11,11 @@ class TestPyramidTemplate(unittest.TestCase): inst.pre('command', 'output dir', vars) self.assertTrue(vars['random_string']) self.assertEqual(vars['package_logger'], 'one') + + def test_pre_site(self): + inst = self._makeOne() + vars = {'package':'site'} + self.assertRaises(ValueError, inst.pre, 'command', 'output dir', vars) def test_pre_root(self): inst = self._makeOne() diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index 3275f7804..d580203af 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -5,7 +5,7 @@ class DummyTweens(object): self.name_to_alias = {} def implicit(self): return self._implicit - + class Dummy: pass @@ -31,6 +31,11 @@ class DummyInteractor: self.banner = banner self.local = local +class DummyBPythonShell: + def __call__(self, locals_, banner): + self.locals_ = locals_ + self.banner = banner + class DummyIPShell(object): IP = Dummy() IP.BANNER = 'foo' @@ -72,7 +77,7 @@ class DummyRoute(object): def match(self, route): return self.matchdict - + class DummyRequest: application_url = 'http://example.com:5432' script_name = '' diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index cdd0daf2e..363808a1e 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -71,6 +71,22 @@ class TestPCreateCommand(unittest.TestCase): scaffold.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro'}) + def test_known_scaffold_absolute_path(self): + import os + path = os.path.abspath('Distro') + cmd = self._makeOne('-s', 'dummy', path) + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + result = cmd.run() + self.assertEqual(result, True) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro'}) + def test_known_scaffold_multiple_rendered(self): import os cmd = self._makeOne('-s', 'dummy1', '-s', 'dummy2', 'Distro') diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index af6ff19d0..328d1001d 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -43,6 +43,18 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(len(L), 3) self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>']) + def test_route_with_no_slash_prefix(self): + command = self._makeOne() + route = dummy.DummyRoute('a', 'a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, None) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>']) + def test_single_route_no_views_registered(self): from zope.interface import Interface from pyramid.registry import Registry diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index e38da2077..765042152 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -21,7 +21,7 @@ class TestPShellCommand(unittest.TestCase): if patch_options: class Options(object): pass self.options = Options() - self.options.disable_ipython = True + self.options.python_shell = '' self.options.setup = None cmd.options = self.options return cmd @@ -34,6 +34,14 @@ class TestPShellCommand(unittest.TestCase): self.assertEqual(interact.local, {'foo': 'bar'}) self.assertTrue('a help message' in interact.banner) + def test_make_bpython_shell(self): + command = self._makeOne() + bpython = dummy.DummyBPythonShell() + shell = command.make_bpython_shell(bpython) + shell({'foo': 'bar'}, 'a help message') + self.assertEqual(bpython.locals_, {'foo': 'bar'}) + self.assertTrue('a help message' in bpython.banner) + def test_make_ipython_v0_11_shell(self): command = self._makeOne() ipshell_factory = dummy.DummyIPShellFactory() @@ -58,6 +66,7 @@ class TestPShellCommand(unittest.TestCase): shell = dummy.DummyShell() command.make_ipython_v0_11_shell = lambda: None command.make_ipython_v0_10_shell = lambda: None + command.make_bpython_shell = lambda: None command.make_default_shell = lambda: shell command.run() self.assertTrue(self.config_factory.parser) @@ -73,14 +82,15 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) - def test_command_loads_default_shell_with_ipython_disabled(self): + def test_command_loads_default_shell_with_unknown_shell(self): command = self._makeOne() shell = dummy.DummyShell() bad_shell = dummy.DummyShell() command.make_ipython_v0_11_shell = lambda: bad_shell command.make_ipython_v0_10_shell = lambda: bad_shell + command.make_bpython_shell = lambda: bad_shell command.make_default_shell = lambda: shell - command.options.disable_ipython = True + command.options.python_shell = 'unknow_python_shell' command.run() self.assertTrue(self.config_factory.parser) self.assertEqual(self.config_factory.parser.filename, @@ -101,8 +111,9 @@ class TestPShellCommand(unittest.TestCase): shell = dummy.DummyShell() command.make_ipython_v0_11_shell = lambda: shell command.make_ipython_v0_10_shell = lambda: None + command.make_bpython_shell = lambda: None command.make_default_shell = lambda: None - command.options.disable_ipython = False + command.options.python_shell = 'ipython' command.run() self.assertTrue(self.config_factory.parser) self.assertEqual(self.config_factory.parser.filename, @@ -122,8 +133,9 @@ class TestPShellCommand(unittest.TestCase): shell = dummy.DummyShell() command.make_ipython_v0_11_shell = lambda: None command.make_ipython_v0_10_shell = lambda: shell + command.make_bpython_shell = lambda: None command.make_default_shell = lambda: None - command.options.disable_ipython = False + command.options.python_shell = 'ipython' command.run() self.assertTrue(self.config_factory.parser) self.assertEqual(self.config_factory.parser.filename, @@ -138,6 +150,76 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) + def test_command_loads_bpython_shell(self): + command = self._makeOne() + shell = dummy.DummyBPythonShell() + command.make_ipython_v0_11_shell = lambda: None + command.make_ipython_v0_10_shell = lambda: None + command.make_bpython_shell = lambda: shell + command.options.python_shell = 'bpython' + command.run() + self.assertTrue(self.config_factory.parser) + self.assertEqual(self.config_factory.parser.filename, + '/foo/bar/myapp.ini') + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.locals_, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.banner) + + def test_shell_ipython_ordering(self): + command = self._makeOne() + shell0_11 = dummy.DummyShell() + shell0_10 = dummy.DummyShell() + command.make_ipython_v0_11_shell = lambda: shell0_11 + command.make_ipython_v0_10_shell = lambda: shell0_10 + command.make_bpython_shell = lambda: None + shell = command.make_shell() + self.assertEqual(shell, shell0_11) + + command.options.python_shell = 'ipython' + shell = command.make_shell() + self.assertEqual(shell, shell0_11) + + def test_shell_ordering(self): + command = self._makeOne() + ipshell = dummy.DummyShell() + bpshell = dummy.DummyShell() + dshell = dummy.DummyShell() + command.make_ipython_v0_11_shell = lambda: None + command.make_ipython_v0_10_shell = lambda: None + command.make_bpython_shell = lambda: None + command.make_default_shell = lambda: dshell + + shell = command.make_shell() + self.assertEqual(shell, dshell) + + command.options.python_shell = 'ipython' + shell = command.make_shell() + self.assertEqual(shell, dshell) + + command.options.python_shell = 'bpython' + shell = command.make_shell() + self.assertEqual(shell, dshell) + + command.make_ipython_v0_11_shell = lambda: ipshell + command.make_bpython_shell = lambda: bpshell + command.options.python_shell = 'ipython' + shell = command.make_shell() + self.assertEqual(shell, ipshell) + + command.options.python_shell = 'bpython' + shell = command.make_shell() + self.assertEqual(shell, bpshell) + + command.options.python_shell = 'python' + shell = command.make_shell() + self.assertEqual(shell, dshell) + def test_command_loads_custom_items(self): command = self._makeOne() model = dummy.Dummy() diff --git a/pyramid/tests/test_settings.py b/pyramid/tests/test_settings.py index d02b3cd3e..2ef15f62a 100644 --- a/pyramid/tests/test_settings.py +++ b/pyramid/tests/test_settings.py @@ -81,9 +81,9 @@ class Test_aslist_cronly(unittest.TestCase): self.assertEqual(result, ['abc', 'def']) class Test_aslist(unittest.TestCase): - def _callFUT(self, val): + def _callFUT(self, val, **kw): from pyramid.settings import aslist - return aslist(val) + return aslist(val, **kw) def test_with_list(self): result = self._callFUT(['abc', 'def']) @@ -100,3 +100,7 @@ class Test_aslist(unittest.TestCase): def test_with_string_crsep_spacesep(self): result = self._callFUT(' abc\n def ghi') self.assertEqual(result, ['abc', 'def', 'ghi']) + + def test_with_string_crsep_spacesep_no_flatten(self): + result = self._callFUT(' abc\n def ghi ', flatten=False) + self.assertEqual(result, ['abc', 'def ghi']) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index a04a47397..bd2c2adef 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -160,6 +160,24 @@ class Test_static_view_use_subpath_False(unittest.TestCase): response = inst(context, request) self.assertEqual(response.status, '404 Not Found') + def test_resource_with_content_encoding(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/arcs.svg.tgz'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.content_type, 'application/x-tar') + self.assertEqual(response.content_encoding, 'gzip') + + def test_resource_no_content_encoding(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.content_type, 'text/html') + self.assertEqual(response.content_encoding, None) + class Test_static_view_use_subpath_True(unittest.TestCase): def _getTargetClass(self): from pyramid.static import static_view diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 05ef36fe9..5b0073b81 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -894,6 +894,11 @@ class TestDummySession(unittest.TestCase): self.assertEqual(token, 'token') self.assertTrue('_csrft_' in session) + def test_get_csrf_token_generates_token(self): + session = self._makeOne() + token = session.get_csrf_token() + self.assertNotEqual(token, None) + self.assertTrue(len(token) >= 1) from zope.interface import Interface from zope.interface import implementer diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 57bcd08d7..b9a9d1960 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,189 +1,6 @@ import unittest from pyramid.compat import PY3 -class TestDottedNameResolver(unittest.TestCase): - def _makeOne(self, package=None): - from pyramid.util import DottedNameResolver - return DottedNameResolver(package) - - def config_exc(self, func, *arg, **kw): - from pyramid.exceptions import ConfigurationError - try: - func(*arg, **kw) - except ConfigurationError as e: - return e - else: - raise AssertionError('Invalid not raised') # pragma: no cover - - def test_zope_dottedname_style_resolve_builtin(self): - typ = self._makeOne() - if PY3: # pragma: no cover - result = typ._zope_dottedname_style('builtins.str') - else: - result = typ._zope_dottedname_style('__builtin__.str') - self.assertEqual(result, str) - - def test_zope_dottedname_style_resolve_absolute(self): - typ = self._makeOne() - result = typ._zope_dottedname_style( - 'pyramid.tests.test_util.TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test_zope_dottedname_style_irrresolveable_absolute(self): - typ = self._makeOne() - self.assertRaises(ImportError, typ._zope_dottedname_style, - 'pyramid.test_util.nonexisting_name') - - def test__zope_dottedname_style_resolve_relative(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._zope_dottedname_style( - '.test_util.TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__zope_dottedname_style_resolve_relative_leading_dots(self): - import pyramid.tests.test_configuration - typ = self._makeOne(package=pyramid.tests) - result = typ._zope_dottedname_style( - '..tests.test_util.TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__zope_dottedname_style_resolve_relative_is_dot(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._zope_dottedname_style('.') - self.assertEqual(result, pyramid.tests) - - def test__zope_dottedname_style_irresolveable_relative_is_dot(self): - typ = self._makeOne() - e = self.config_exc(typ._zope_dottedname_style, '.') - self.assertEqual( - e.args[0], - "relative name '.' irresolveable without package") - - def test_zope_dottedname_style_resolve_relative_nocurrentpackage(self): - typ = self._makeOne() - e = self.config_exc(typ._zope_dottedname_style, '.whatever') - self.assertEqual( - e.args[0], - "relative name '.whatever' irresolveable without package") - - def test_zope_dottedname_style_irrresolveable_relative(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - self.assertRaises(ImportError, typ._zope_dottedname_style, - '.notexisting') - - def test__zope_dottedname_style_resolveable_relative(self): - import pyramid - typ = self._makeOne(package=pyramid) - result = typ._zope_dottedname_style('.tests') - from pyramid import tests - self.assertEqual(result, tests) - - def test__zope_dottedname_style_irresolveable_absolute(self): - typ = self._makeOne() - self.assertRaises( - ImportError, - typ._zope_dottedname_style, 'pyramid.fudge.bar') - - def test__zope_dottedname_style_resolveable_absolute(self): - typ = self._makeOne() - result = typ._zope_dottedname_style( - 'pyramid.tests.test_util.TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__pkg_resources_style_resolve_absolute(self): - typ = self._makeOne() - result = typ._pkg_resources_style( - 'pyramid.tests.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__pkg_resources_style_irrresolveable_absolute(self): - typ = self._makeOne() - self.assertRaises(ImportError, typ._pkg_resources_style, - 'pyramid.tests:nonexisting') - - def test__pkg_resources_style_resolve_relative(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._pkg_resources_style( - '.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__pkg_resources_style_resolve_relative_is_dot(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._pkg_resources_style('.') - self.assertEqual(result, pyramid.tests) - - def test__pkg_resources_style_resolve_relative_nocurrentpackage(self): - typ = self._makeOne() - from pyramid.exceptions import ConfigurationError - self.assertRaises(ConfigurationError, typ._pkg_resources_style, - '.whatever') - - def test__pkg_resources_style_irrresolveable_relative(self): - import pyramid - typ = self._makeOne(package=pyramid) - self.assertRaises(ImportError, typ._pkg_resources_style, - ':notexisting') - - def test_resolve_not_a_string(self): - typ = self._makeOne() - e = self.config_exc(typ.resolve, None) - self.assertEqual(e.args[0], 'None is not a string') - - def test_resolve_using_pkgresources_style(self): - typ = self._makeOne() - result = typ.resolve( - 'pyramid.tests.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test_resolve_using_zope_dottedname_style(self): - typ = self._makeOne() - result = typ.resolve( - 'pyramid.tests.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test_resolve_missing_raises(self): - typ = self._makeOne() - self.assertRaises(ImportError, typ.resolve, 'cant.be.found') - - def test_ctor_string_module_resolveable(self): - import pyramid.tests - typ = self._makeOne('pyramid.tests.test_util') - self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') - - def test_ctor_string_package_resolveable(self): - import pyramid.tests - typ = self._makeOne('pyramid.tests') - self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') - - def test_ctor_string_irresolveable(self): - from pyramid.config import ConfigurationError - self.assertRaises(ConfigurationError, self._makeOne, 'cant.be.found') - - def test_ctor_module(self): - import pyramid.tests - import pyramid.tests.test_util - typ = self._makeOne(pyramid.tests.test_util) - self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') - - def test_ctor_package(self): - import pyramid.tests - typ = self._makeOne(pyramid.tests) - self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') - - def test_ctor_None(self): - typ = self._makeOne(None) - self.assertEqual(typ.package, None) - self.assertEqual(typ.package_name, None) - class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet @@ -255,5 +72,78 @@ class Test_WeakOrderedSet(unittest.TestCase): self.assertEqual(list(wos), []) self.assertEqual(wos.last, None) +class Test_object_description(unittest.TestCase): + def _callFUT(self, object): + from pyramid.util import object_description + return object_description(object) + + def test_string(self): + self.assertEqual(self._callFUT('abc'), 'abc') + + def test_int(self): + self.assertEqual(self._callFUT(1), '1') + + def test_bool(self): + self.assertEqual(self._callFUT(True), 'True') + + def test_None(self): + self.assertEqual(self._callFUT(None), 'None') + + def test_float(self): + self.assertEqual(self._callFUT(1.2), '1.2') + + def test_tuple(self): + self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") + + def test_set(self): + if PY3: # pragma: no cover + self.assertEqual(self._callFUT(set(['a'])), "{'a'}") + else: # pragma: no cover + self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") + + def test_list(self): + self.assertEqual(self._callFUT(['a']), "['a']") + + def test_dict(self): + self.assertEqual(self._callFUT({'a':1}), "{'a': 1}") + + def test_nomodule(self): + o = object() + self.assertEqual(self._callFUT(o), 'object %s' % str(o)) + + def test_module(self): + import pyramid + self.assertEqual(self._callFUT(pyramid), 'module pyramid') + + def test_method(self): + self.assertEqual( + self._callFUT(self.test_method), + 'method test_method of class pyramid.tests.test_util.' + 'Test_object_description') + + def test_class(self): + self.assertEqual( + self._callFUT(self.__class__), + 'class pyramid.tests.test_util.Test_object_description') + + def test_function(self): + self.assertEqual( + self._callFUT(dummyfunc), + 'function pyramid.tests.test_util.dummyfunc') + + def test_instance(self): + inst = Dummy() + self.assertEqual( + self._callFUT(inst), + "object %s" % str(inst)) + + def test_shortened_repr(self): + inst = ['1'] * 1000 + self.assertEqual( + self._callFUT(inst), + str(inst)[:100] + ' ... ]') + +def dummyfunc(): pass + class Dummy(object): pass diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 1bdb066c0..0d00e65c6 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -570,6 +570,40 @@ class Test_static(unittest.TestCase): view = self._makeOne(path, None) self.assertEqual(view.docroot, 'fixtures') +class Test_view_defaults(unittest.TestCase): + def test_it(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + self.assertEqual(Foo.__view_defaults__['route_name'],'abc') + self.assertEqual(Foo.__view_defaults__['renderer'],'def') + + def test_it_inheritance_not_overridden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'abc') + self.assertEqual(Bar.__view_defaults__['renderer'],'def') + + def test_it_inheritance_overriden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults(route_name='ghi') + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'ghi') + self.assertEqual(Bar.__view_defaults__['renderer'], None) + + def test_it_inheritance_overriden_empty(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults() + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'], None) + self.assertEqual(Bar.__view_defaults__['renderer'], None) + class ExceptionResponse(Exception): status = '404 Not Found' app_iter = ['Not Found'] diff --git a/pyramid/tests/test_wsgi.py b/pyramid/tests/test_wsgi.py index 06bcf1cb2..63499b43b 100644 --- a/pyramid/tests/test_wsgi.py +++ b/pyramid/tests/test_wsgi.py @@ -12,6 +12,14 @@ class WSGIAppTests(unittest.TestCase): response = decorator(context, request) self.assertEqual(response, dummyapp) + def test_decorator_object_instance(self): + context = DummyContext() + request = DummyRequest() + app = DummyApp() + decorator = self._callFUT(app) + response = decorator(context, request) + self.assertEqual(response, app) + class WSGIApp2Tests(unittest.TestCase): def _callFUT(self, app): from pyramid.wsgi import wsgiapp2 @@ -84,9 +92,25 @@ class WSGIApp2Tests(unittest.TestCase): self.assertEqual(request.environ['PATH_INFO'], '/') self.assertEqual(request.environ['SCRIPT_NAME'], '') + def test_decorator_on_callable_object_instance(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/'} + app = DummyApp() + decorator = self._callFUT(app) + response = decorator(context, request) + self.assertEqual(response, app) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo') + def dummyapp(environ, start_response): """ """ +class DummyApp(object): + def __call__(self, environ, start_response): + """ """ + class DummyContext: pass diff --git a/pyramid/traversal.py b/pyramid/traversal.py index ee6b5fb7a..cd624fd30 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -5,20 +5,25 @@ from zope.interface.interfaces import IInterface from repoze.lru import lru_cache -from pyramid.interfaces import IContextURL -from pyramid.interfaces import IRequestFactory -from pyramid.interfaces import ITraverser -from pyramid.interfaces import VH_ROOT_KEY - -from pyramid.compat import PY3 -from pyramid.compat import native_ -from pyramid.compat import text_ -from pyramid.compat import bytes_ -from pyramid.compat import ascii_native_ -from pyramid.compat import text_type -from pyramid.compat import binary_type -from pyramid.compat import url_unquote_native -from pyramid.compat import is_nonstr_iter +from pyramid.interfaces import ( + IContextURL, + IRequestFactory, + ITraverser, + VH_ROOT_KEY, + ) + +from pyramid.compat import ( + PY3, + native_, + text_, + bytes_, + ascii_native_, + text_type, + binary_type, + url_unquote_native, + is_nonstr_iter, + ) + from pyramid.encode import url_quote from pyramid.exceptions import URLDecodeError from pyramid.location import lineage diff --git a/pyramid/tweens.py b/pyramid/tweens.py index 65d7c3919..73a95e1b8 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -1,6 +1,9 @@ import sys -from pyramid.interfaces import IExceptionViewClassifier -from pyramid.interfaces import IView + +from pyramid.interfaces import ( + IExceptionViewClassifier, + IView, + ) from zope.interface import providedBy diff --git a/pyramid/url.py b/pyramid/url.py index 7a7dd3b4c..afb602d3a 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -4,17 +4,24 @@ import os from repoze.lru import lru_cache -from pyramid.interfaces import IContextURL -from pyramid.interfaces import IRoutesMapper -from pyramid.interfaces import IStaticURLInfo - -from pyramid.compat import native_ -from pyramid.compat import text_type +from pyramid.interfaces import ( + IContextURL, + IRoutesMapper, + IStaticURLInfo, + ) + +from pyramid.compat import ( + native_, + text_type, + ) from pyramid.encode import urlencode from pyramid.path import caller_package from pyramid.threadlocal import get_current_registry -from pyramid.traversal import TraversalContextURL -from pyramid.traversal import quote_path_segment + +from pyramid.traversal import ( + TraversalContextURL, + quote_path_segment, + ) class URLMethodsMixin(object): """ Request methods mixin for BaseRequest having to do with URL @@ -184,10 +191,10 @@ class URLMethodsMixin(object): Calling ``request.route_path('route')`` is the same as calling ``request.route_url('route', _app_url=request.script_name)``. - :meth:`pyramid.request.Request.route_path` is, in fact, implemented - in terms of `:meth:`pyramid.request.Request.route_url` in just this - way. As a result, any ``_app_url`` passed within the ``**kw`` values - to ``route_path`` will be ignored. + :meth:`pyramid.request.Request.route_path` is, in fact, + implemented in terms of :meth:`pyramid.request.Request.route_url` + in just this way. As a result, any ``_app_url`` passed within the + ``**kw`` values to ``route_path`` will be ignored. """ kw['_app_url'] = self.script_name return self.route_url(route_name, *elements, **kw) @@ -340,10 +347,9 @@ class URLMethodsMixin(object): The ``path`` argument points at a file or directory on disk which a URL should be generated for. The ``path`` may be either a - relative path (e.g. ``static/foo.css``) or a :term:`asset - specification` (e.g. ``mypackage:static/foo.css``). A ``path`` - may not be an absolute filesystem path (a :exc:`ValueError` will - be raised if this function is supplied with an absolute path). + relative path (e.g. ``static/foo.css``) or an absolute path (e.g. + ``/abspath/to/static/foo.css``) or a :term:`asset specification` + (e.g. ``mypackage:static/foo.css``). The purpose of the ``**kw`` argument is the same as the purpose of the :meth:`pyramid.request.Request.route_url` ``**kw`` argument. See @@ -380,7 +386,7 @@ class URLMethodsMixin(object): and port) for a static resource. This function accepts the same argument as - :meth:`pyramid.request.Request.current_static_url` and performs the + :meth:`pyramid.request.Request.static_url` and performs the same duty. It just omits the host, port, and scheme information in the return value; only the script_name, path, query parameters, and anchor data are present in the returned string. diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 662615845..c7520b8d2 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -1,18 +1,26 @@ import re from zope.interface import implementer -from pyramid.interfaces import IRoutesMapper -from pyramid.interfaces import IRoute - -from pyramid.compat import native_ -from pyramid.compat import bytes_ -from pyramid.compat import text_type -from pyramid.compat import string_types -from pyramid.compat import is_nonstr_iter -from pyramid.compat import url_quote +from pyramid.interfaces import ( + IRoutesMapper, + IRoute, + ) + +from pyramid.compat import ( + native_, + bytes_, + text_type, + string_types, + is_nonstr_iter, + url_quote, + ) + from pyramid.exceptions import URLDecodeError -from pyramid.traversal import traversal_path_info -from pyramid.traversal import quote_path_segment + +from pyramid.traversal import ( + traversal_path_info, + quote_path_segment, + ) _marker = object() diff --git a/pyramid/util.py b/pyramid/util.py index a43b50aef..76968bbbd 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,149 +1,18 @@ -import pkg_resources -import sys +import inspect import weakref -from pyramid.compat import string_types -from pyramid.exceptions import ConfigurationError -from pyramid.path import package_of +from pyramid.compat import ( + integer_types, + string_types, + text_, + PY3, + ) -class DottedNameResolver(object): - """ This class resolves dotted name references to 'global' Python - objects (objects which can be imported) to those objects. +from pyramid.path import DottedNameResolver as _DottedNameResolver - Two dotted name styles are supported during deserialization: - - - ``pkg_resources``-style dotted names where non-module attributes - of a package are separated from the rest of the path using a ':' - e.g. ``package.module:attr``. - - - ``zope.dottedname``-style dotted names where non-module - attributes of a package are separated from the rest of the path - using a '.' e.g. ``package.module.attr``. - - These styles can be used interchangeably. If the serialization - contains a ``:`` (colon), the ``pkg_resources`` resolution - mechanism will be chosen, otherwise the ``zope.dottedname`` - resolution mechanism will be chosen. - - The constructor accepts a single argument named ``package`` which - should be a one of: - - - a Python module or package object - - - A fully qualified (not relative) dotted name to a module or package - - - The value ``None`` - - The ``package`` is used when relative dotted names are supplied to - the resolver's ``resolve`` and ``maybe_resolve`` methods. A - dotted name which has a ``.`` (dot) or ``:`` (colon) as its first - character is treated as relative. - - If the value ``None`` is supplied as the package name, the - resolver will only be able to resolve fully qualified (not - relative) names. Any attempt to resolve a relative name when the - ``package`` is ``None`` will result in an - :exc:`pyramid.config.ConfigurationError` exception. - - If a *module* or *module name* (as opposed to a package or package - name) is supplied as ``package``, its containing package is - computed and this package used to derive the package name (all - names are resolved relative to packages, never to modules). For - example, if the ``package`` argument to this type was passed the - string ``xml.dom.expatbuilder``, and ``.mindom`` is supplied to - the ``resolve`` method, the resulting import would be for - ``xml.minidom``, because ``xml.dom.expatbuilder`` is a module - object, not a package object. - - If a *package* or *package name* (as opposed to a module or module - name) is supplied as ``package``, this package will be used to - relative compute dotted names. For example, if the ``package`` - argument to this type was passed the string ``xml.dom``, and - ``.minidom`` is supplied to the ``resolve`` method, the resulting - import would be for ``xml.minidom``. - - When a dotted name cannot be resolved, a - :class:`pyramid.exceptions.ConfigurationError` error is raised. - """ - def __init__(self, package): - if package is None: - self.package_name = None - self.package = None - else: - if isinstance(package, string_types): - try: - __import__(package) - except ImportError: - raise ConfigurationError( - 'The dotted name %r cannot be imported' % (package,)) - package = sys.modules[package] - self.package = package_of(package) - self.package_name = self.package.__name__ - - def _pkg_resources_style(self, value): - """ package.module:attr style """ - if value.startswith('.') or value.startswith(':'): - if not self.package_name: - raise ConfigurationError( - 'relative name %r irresolveable without ' - 'package_name' % (value,)) - if value in ['.', ':']: - value = self.package_name - else: - value = self.package_name + value - return pkg_resources.EntryPoint.parse( - 'x=%s' % value).load(False) - - def _zope_dottedname_style(self, value): - """ package.module.attr style """ - module = self.package_name - if not module: - module = None - if value == '.': - if module is None: - raise ConfigurationError( - 'relative name %r irresolveable without package' % (value,) - ) - name = module.split('.') - else: - name = value.split('.') - if not name[0]: - if module is None: - raise ConfigurationError( - 'relative name %r irresolveable without ' - 'package' % (value,) - ) - module = module.split('.') - name.pop(0) - while not name[0]: - module.pop() - name.pop(0) - name = module + name - - used = name.pop(0) - found = __import__(used) - for n in name: - used += '.' + n - try: - found = getattr(found, n) - except AttributeError: - __import__(used) - found = getattr(found, n) # pragma: no cover - - return found - - def resolve(self, dotted): - if not isinstance(dotted, string_types): - raise ConfigurationError('%r is not a string' % (dotted,)) - return self.maybe_resolve(dotted) - - def maybe_resolve(self, dotted): - if isinstance(dotted, string_types): - if ':' in dotted: - return self._pkg_resources_style(dotted) - else: - return self._zope_dottedname_style(dotted) - return dotted +class DottedNameResolver(_DottedNameResolver): + def __init__(self, package=None): # default to package = None for bw compat + return _DottedNameResolver.__init__(self, package) class WeakOrderedSet(object): """ Maintain a set of items. @@ -208,3 +77,91 @@ class WeakOrderedSet(object): oid = self._order[-1] return self._items[oid]() +def strings_differ(string1, string2): + """Check whether two strings differ while avoiding timing attacks. + + This function returns True if the given strings differ and False + if they are equal. It's careful not to leak information about *where* + they differ as a result of its running time, which can be very important + to avoid certain timing-related crypto attacks: + + http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf + + """ + if len(string1) != len(string2): + return True + + invalid_bits = 0 + for a, b in zip(string1, string2): + invalid_bits += a != b + + return invalid_bits != 0 + +def object_description(object): + """ Produce a human-consumable text description of ``object``, + usually involving a Python dotted name. For example: + + .. code-block:: python + + >>> object_description(None) + u'None' + >>> from xml.dom import minidom + >>> object_description(minidom) + u'module xml.dom.minidom' + >>> object_description(minidom.Attr) + u'class xml.dom.minidom.Attr' + >>> object_description(minidom.Attr.appendChild) + u'method appendChild of class xml.dom.minidom.Attr' + >>> + + If this method cannot identify the type of the object, a generic + description ala ``object <object.__name__>`` will be returned. + + If the object passed is already a string, it is simply returned. If it + is a boolean, an integer, a list, a tuple, a set, or ``None``, a + (possibly shortened) string representation is returned. + """ + if isinstance(object, string_types): + return text_(object) + if isinstance(object, integer_types): + return text_(str(object)) + if isinstance(object, (bool, float, type(None))): + return text_(str(object)) + if isinstance(object, set): + if PY3: # pragma: no cover + return shortrepr(object, '}') + else: + return shortrepr(object, ')') + if isinstance(object, tuple): + return shortrepr(object, ')') + if isinstance(object, list): + return shortrepr(object, ']') + if isinstance(object, dict): + return shortrepr(object, '}') + module = inspect.getmodule(object) + if module is None: + return text_('object %s' % str(object)) + modulename = module.__name__ + if inspect.ismodule(object): + return text_('module %s' % modulename) + if inspect.ismethod(object): + oself = getattr(object, '__self__', None) + if oself is None: # pragma: no cover + oself = getattr(object, 'im_self', None) + return text_('method %s of class %s.%s' % + (object.__name__, modulename, + oself.__class__.__name__)) + + if inspect.isclass(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return text_('class %s' % dottedname) + if inspect.isfunction(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return text_('function %s' % dottedname) + return text_('object %s' % str(object)) + +def shortrepr(object, closer): + r = str(object) + if len(r) > 100: + r = r[:100] + ' ... %s' % closer + return r diff --git a/pyramid/view.py b/pyramid/view.py index 13d5cfe7b..eae56a661 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -3,13 +3,17 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated -from pyramid.interfaces import IRoutesMapper -from pyramid.interfaces import IView -from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import ( + IRoutesMapper, + IView, + IViewClassifier, + ) from pyramid.compat import map_ -from pyramid.httpexceptions import HTTPFound -from pyramid.httpexceptions import default_exceptionresponse_view +from pyramid.httpexceptions import ( + HTTPFound, + default_exceptionresponse_view, + ) from pyramid.path import caller_package from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -156,11 +160,12 @@ class view_config(object): backwards compatibility purposes, as the name :class:`pyramid.view.bfg_view`. - The following arguments are supported as arguments to + The following arguments are supported to :class:`pyramid.view.view_config`: ``context``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, - ``custom_predicates``, ``decorator``, ``mapper``, and ``http_cache``. + ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, + and ``match_param``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. @@ -218,6 +223,19 @@ class view_config(object): bfg_view = view_config # bw compat (forever) +class view_defaults(view_config): + """ A class :term:`decorator` which, when applied to a class, will + provide defaults for all view configurations that use the class. This + decorator accepts all the arguments accepted by + :class:`pyramid.config.view_config`, and each has the same meaning. + + See :ref:`view_defaults` for more information. + """ + + def __call__(self, wrapped): + wrapped.__view_defaults__ = self.__dict__.copy() + return wrapped + class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use diff --git a/pyramid/wsgi.py b/pyramid/wsgi.py index 3bbe31790..5fa23d554 100644 --- a/pyramid/wsgi.py +++ b/pyramid/wsgi.py @@ -31,7 +31,11 @@ def wsgiapp(wrapped): """ def decorator(context, request): return request.get_response(wrapped) - return wraps(wrapped)(decorator) + + # Support case where wrapped is a callable object instance + if getattr(wrapped, '__name__', None): + return wraps(wrapped)(decorator) + return wraps(wrapped, ('__module__', '__doc__'))(decorator) def wsgiapp2(wrapped): """ Decorator to turn a WSGI application into a :app:`Pyramid` @@ -67,4 +71,8 @@ def wsgiapp2(wrapped): def decorator(context, request): return call_app_with_subpath_as_path_info(request, wrapped) - return wraps(wrapped)(decorator) + + # Support case where wrapped is a callable object instance + if getattr(wrapped, '__name__', None): + return wraps(wrapped)(decorator) + return wraps(wrapped, ('__module__', '__doc__'))(decorator) diff --git a/rtd.txt b/rtd.txt new file mode 100644 index 000000000..9de7ff3bb --- /dev/null +++ b/rtd.txt @@ -0,0 +1,3 @@ +repoze.sphinx.autointerface +repoze.lru + @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3dev', + version='1.4dev', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, @@ -67,6 +67,8 @@ setup(name='pyramid', "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Pylons", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI", @@ -87,7 +89,6 @@ setup(name='pyramid', [pyramid.scaffold] starter=pyramid.scaffolds:StarterProjectTemplate zodb=pyramid.scaffolds:ZODBProjectTemplate - routesalchemy=pyramid.scaffolds:RoutesAlchemyProjectTemplate alchemy=pyramid.scaffolds:AlchemyProjectTemplate [console_scripts] bfg2pyramid = pyramid.fixers.fix_bfg_imports:main @@ -99,6 +100,7 @@ setup(name='pyramid', ptweens = pyramid.scripts.ptweens:main [paste.server_runner] wsgiref = pyramid.scripts.pserve:wsgiref_server_runner + cherrypy = pyramid.scripts.pserve:cherrypy_server_runner """ ) @@ -6,7 +6,6 @@ envlist = commands = python setup.py test -q deps = - https://github.com/Pylons/webob/zipball/master zope.component Sphinx repoze.sphinx.autointerface @@ -17,7 +16,6 @@ deps = commands = python setup.py test -q deps = - https://github.com/Pylons/webob/zipball/master WebTest virtualenv @@ -27,7 +25,6 @@ basepython = commands = python setup.py nosetests --with-xunit --with-xcoverage deps = - https://github.com/Pylons/webob/zipball/master zope.component Sphinx WebTest |
