diff options
| author | Amos Latteier <amos@latteier.com> | 2016-06-30 10:17:43 -0700 |
|---|---|---|
| committer | Amos Latteier <amos@latteier.com> | 2016-06-30 10:17:43 -0700 |
| commit | 35209e4ac53520e1159bd8a6b47128f38a75db18 (patch) | |
| tree | 85b0f7e8e3794d8980dd10c10b7e57a862e694aa | |
| parent | 3fd41dcb5a94a73f43862f6d5c063af7b54e6ff3 (diff) | |
| parent | bcba92d1b23745d736bdcbc9b799929de382db7b (diff) | |
| download | pyramid-35209e4ac53520e1159bd8a6b47128f38a75db18.tar.gz pyramid-35209e4ac53520e1159bd8a6b47128f38a75db18.tar.bz2 pyramid-35209e4ac53520e1159bd8a6b47128f38a75db18.zip | |
Merge branch 'master' into exception_only
24 files changed, 129 insertions, 663 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c0681053d..4fdd7bf2f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,16 @@ unreleased Backward Incompatibilities -------------------------- + - Following the Pyramid deprecation period (1.6 -> 1.8), + daemon support for pserve has been removed. This includes removing the + daemon commands (start, stop, restart, status) as well as the following + arguments: + --daemon --pid-file --log-file --monitor-restart --status --user --group + --stop-daemon + + To run your server as a daemon you should use a process manager instead of + pserve. + Features -------- diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 762793d0b..25ccf6838 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -270,3 +270,11 @@ Contributors - Vincent Férotin, 2016/05/08 - Berker Peksag, 2016/05/16 + +- Zack Brunson, 2016/06/02 + +- Marco Martinez, 2016/06/02 + +- Cris Ewing, 2016/06/03 + +- Jean-Christophe Bohin, 2016/06/13
\ No newline at end of file diff --git a/README.rst b/README.rst index dec7e03d0..35c335d9c 100644 --- a/README.rst +++ b/README.rst @@ -17,11 +17,28 @@ Pyramid :target: https://webchat.freenode.net/?channels=pyramid :alt: IRC Freenode -Pyramid is a small, fast, down-to-earth, open source Python web framework. It -makes real-world web application development and deployment more fun, more -predictable, and more productive. +`Pyramid <https://trypyramid.com/>`_ is a small, fast, down-to-earth, open +source Python web framework. It makes real-world web application development +and deployment more fun, more predictable, and more productive. -Pyramid is produced by the `Pylons Project <http://pylonsproject.org/>`_. +.. code-block:: python + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello %(name)s!' % request.matchdict) + + if __name__ == '__main__': + config = Configurator() + 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, app) + server.serve_forever() + +Pyramid is a project of the `Pylons Project <http://www.pylonsproject.org/>`_. Support and Documentation ------------------------- @@ -46,6 +63,6 @@ Pyramid is offered under the BSD-derived `Repoze Public License Authors ------- -Pyramid is made available by `Agendaless Consulting <http://agendaless.com>`_ -and a team of contributors. - +Pyramid is made available by `Agendaless Consulting <https://agendaless.com>`_ +and a team of `contributors +<https://github.com/Pylons/pyramid/graphs/contributors>`_. diff --git a/contributing.md b/contributing.md index ae201d370..b5f17ae06 100644 --- a/contributing.md +++ b/contributing.md @@ -56,11 +56,15 @@ System](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/install.ht git clone git@github.com:<username>/pyramid.git -3. Add a git remote "upstream" for the cloned fork. +3. Change directories into the cloned repository + + cd pyramid + +4. Add a git remote "upstream" for the cloned fork. git remote add upstream git@github.com:Pylons/pyramid.git -4. Set an environment variable as instructed in the +5. Create a virtual environment and set an environment variable as instructed in the [prerequisites](https://github.com/Pylons/pyramid/blob/master/HACKING.txt#L55-L58). # Mac and Linux @@ -69,36 +73,34 @@ System](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/install.ht # Windows set VENV=c:\hack-on-pyramid\env -5. Try to build the docs in your workspace. +6. Install `tox` into your virtual environment. - # Mac and Linux - $ make clean html SPHINXBUILD=$VENV/bin/sphinx-build + $ $VENV/bin/pip install tox - # Windows - c:\> make clean html SPHINXBUILD=%VENV%\bin\sphinx-build +7. Try to build the docs in your workspace. + + $ $VENV/bin/tox -e docs - If successful, then you can make changes to the documentation. You can - load the built documentation in the `/_build/html/` directory in a web - browser. + When the build finishes, you'll find HTML documentation rendered in + `.tox/docs/html`. An `epub` version will be in `.tox/docs/epub`. And the + result of the tests that are run on the documentation will be in + `.tox/docs/doctest`. -6. From this point forward, follow the typical [git +8. From this point forward, follow the typical [git workflow](https://help.github.com/articles/what-is-a-good-git-workflow/). - Start by pulling from the upstream to get the most current changes. + *Always* start by pulling from the upstream to get the most current changes. git pull upstream master -7. Make a branch, make changes to the docs, and rebuild them as indicated in - step 5. To speed up the build process, you can omit `clean` from the above - command to rebuild only those pages that depend on the files you have - changed. +9. Make a branch, make changes to the docs, and rebuild them as indicated above. -8. Once you are satisfied with your changes and the documentation builds - successfully without errors or warnings, then git commit and push them to - your "origin" repository on GitHub. +10. Once you are satisfied with your changes and the documentation builds + successfully without errors or warnings, then git commit and push them to + your "origin" repository on GitHub. git commit -m "commit message" git push -u origin --all # first time only, subsequent can be just 'git push'. -9. Create a [pull request](https://help.github.com/articles/using-pull-requests/). +11. Create a [pull request](https://help.github.com/articles/using-pull-requests/). -10. Repeat the process starting from Step 6. +12. Repeat the process starting from Step 8. diff --git a/docs/designdefense.rst b/docs/designdefense.rst index f42582e47..5f65671bb 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1009,7 +1009,7 @@ Microframeworks have smaller Hello World programs Self-described "microframeworks" exist. `Bottle <http://bottlepy.org/docs/dev/index.html>`_ and `Flask <http://flask.pocoo.org/>`_ are two that are becoming popular. `Bobo -<http://bobo.digicool.com/en/latest/>`_ doesn't describe itself as a +<https://bobo.readthedocs.io/en/latest/>`_ doesn't describe itself as a microframework, but its intended user base is much the same. Many others exist. We've even (only as a teaching tool, not as any sort of official project) `created one using Pyramid <http://static.repoze.org/casts/videotags.html>`_. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 7cbea113c..77e7fd707 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -290,6 +290,14 @@ properties of the instance. def __init__(self, owner): self.owner = owner +.. warning:: + + Writing ``__acl__`` as properties is discouraged because an + ``AttributeError`` occurring in ``fget`` or ``fset`` will be silently + dismissed (this is consistent with Python ``getattr`` and ``hasattr`` + behaviors). For dynamic ACLs, simply use callables, as documented above. + + .. index:: single: ACE single: access control entry diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 2472ace31..7d37c04df 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -557,7 +557,7 @@ Here is an example of a corresponding ``mypackage.views`` module: @view_config(route_name='idea') def idea_view(request): - return Response(request.matchdict['id']) + return Response(request.matchdict['idea']) @view_config(route_name='user') def user_view(request): diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index b170e5d98..dde91b495 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -1,3 +1,4 @@ + .. _quick_tour: ===================== @@ -70,6 +71,7 @@ step. Here's a tiny application in Pyramid: .. literalinclude:: quick_tour/hello_world/app.py :linenos: + :language: python This simple example is easy to run. Save this as ``app.py`` and run it: @@ -120,6 +122,7 @@ library for request and response handling. In our example above, Pyramid hands Let's see some features of requests and responses in action: .. literalinclude:: quick_tour/requests/app.py + :language: python :pyobject: hello_world In this Pyramid view, we get the URL being visited from ``request.url``. Also @@ -159,6 +162,7 @@ Let's move the views out to their own ``views.py`` module and change the First our revised ``app.py``: .. literalinclude:: quick_tour/views/app.py + :language: python :linenos: We added some more routes, but we also removed the view code. Our views and @@ -169,6 +173,7 @@ We now have a ``views.py`` module that is focused on handling requests and responses: .. literalinclude:: quick_tour/views/views.py + :language: python :linenos: We have four views, each leading to the other. If you start at @@ -214,6 +219,7 @@ What if we want part of the URL to be available as data in my view? We can use this route declaration, for example: .. literalinclude:: quick_tour/routing/app.py + :language: python :linenos: :lines: 6 :lineno-start: 6 @@ -222,6 +228,7 @@ With this, URLs such as ``/howdy/amy/smith`` will assign ``amy`` to ``first`` and ``smith`` to ``last``. We can then use this data in our view: .. literalinclude:: quick_tour/routing/views.py + :language: python :linenos: :lines: 5-8 :lineno-start: 5 @@ -260,6 +267,7 @@ With the package installed, we can include the template bindings into our configuration in ``app.py``: .. literalinclude:: quick_tour/templating/app.py + :language: python :linenos: :lines: 6-8 :lineno-start: 6 @@ -268,6 +276,7 @@ configuration in ``app.py``: Now lets change our ``views.py`` file: .. literalinclude:: quick_tour/templating/views.py + :language: python :linenos: :emphasize-lines: 4,6 @@ -304,6 +313,7 @@ With the package installed, we can include the template bindings into our configuration: .. literalinclude:: quick_tour/jinja2/app.py + :language: python :linenos: :lines: 6-8 :lineno-start: 6 @@ -312,6 +322,7 @@ configuration: The only change in our view is to point the renderer at the ``.jinja2`` file: .. literalinclude:: quick_tour/jinja2/views.py + :language: python :linenos: :lines: 4-6 :lineno-start: 4 @@ -339,9 +350,10 @@ Static assets Of course the Web is more than just markup. You need static assets: CSS, JS, and images. Let's point our web app at a directory from which Pyramid will serve some static assets. First let's make another call to the -:term:`configurator`: +:term:`configurator` in ``app.py``: .. literalinclude:: quick_tour/static_assets/app.py + :language: python :linenos: :lines: 6-8 :lineno-start: 6 @@ -359,7 +371,7 @@ Next make a directory named ``static``, and place ``app.css`` inside: All we need to do now is point to it in the ``<head>`` of our Jinja2 template, ``hello_world.jinja2``: -.. literalinclude:: quick_tour/static_assets/hello_world.jinja2 +.. literalinclude:: quick_tour/static_assets/hello_world_static.jinja2 :language: jinja :linenos: :lines: 4-6 @@ -371,16 +383,16 @@ the site is later moved under ``/somesite/static/``? Or perhaps a web developer changes the arrangement on disk? Pyramid provides a helper to allow flexibility on URL generation: -.. literalinclude:: quick_tour/static_assets/hello_world_static.jinja2 +.. literalinclude:: quick_tour/static_assets/hello_world.jinja2 :language: jinja :linenos: :lines: 4-6 :lineno-start: 4 :emphasize-lines: 2 -By using ``request.static_url`` to generate the full URL to the static -assets, you both ensure you stay in sync with the configuration and -gain refactoring flexibility later. +By using ``request.static_url`` to generate the full URL to the static assets, +you ensure that you stay in sync with the configuration and gain refactoring +flexibility later. .. seealso:: See also: :ref:`Quick Tutorial Static Assets <qtut_static_assets>`, @@ -396,6 +408,7 @@ to update the UI in the browser by requesting server data as JSON. Pyramid supports this with a JSON renderer: .. literalinclude:: quick_tour/json/views.py + :language: python :linenos: :lines: 9- :lineno-start: 9 @@ -408,6 +421,7 @@ We also need to add a route to ``app.py`` so that our app will know how to respond to a request for ``hello.json``. .. literalinclude:: quick_tour/json/app.py + :language: python :linenos: :lines: 6-8 :lineno-start: 6 @@ -437,6 +451,7 @@ The following shows a "Hello World" example with three operations: view a form, save a change, or press the delete button in our ``views.py``: .. literalinclude:: quick_tour/view_classes/views.py + :language: python :linenos: :lines: 7- :lineno-start: 7 @@ -728,6 +743,7 @@ Our unit test passed, although its coverage is incomplete. What did our test look like? .. literalinclude:: quick_tour/package/hello_world/tests.py + :language: python :linenos: Pyramid supplies helpers for test writing, which we use in the test setup and @@ -881,6 +897,7 @@ SQLAlchemy uses "models" for this mapping. The scaffold generated a sample model: .. literalinclude:: quick_tour/sqla_demo/sqla_demo/models/mymodel.py + :language: python :start-after: Start Sphinx Include :end-before: End Sphinx Include @@ -888,6 +905,7 @@ View code, which mediates the logic between web requests and the rest of the system, can then easily get at the data thanks to SQLAlchemy: .. literalinclude:: quick_tour/sqla_demo/sqla_demo/views/default.py + :language: python :start-after: Start Sphinx Include :end-before: End Sphinx Include diff --git a/docs/quick_tour/json/hello_world.jinja2 b/docs/quick_tour/json/hello_world.jinja2 index 4fb9be074..a55865231 100644 --- a/docs/quick_tour/json/hello_world.jinja2 +++ b/docs/quick_tour/json/hello_world.jinja2 @@ -2,7 +2,7 @@ <html lang="en"> <head> <title>Hello World</title> - <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/> + <link rel="stylesheet" href="{{ request.static_url('__main__:static/app.css') }}"/> </head> <body> <h1>Hello {{ name }}!</h1> diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py index 6ffc10a78..76e0fd26b 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import configure_mappers # import all models classes here for sqlalchemy mappers # to pick up -from .mymodel import MyModel # flake8: noqa +from .mymodel import MyModel # noqa # run configure mappers to ensure we avoid any race conditions configure_mappers() diff --git a/docs/quick_tour/static_assets/hello_world.jinja2 b/docs/quick_tour/static_assets/hello_world.jinja2 index 0fb2ce296..a55865231 100644 --- a/docs/quick_tour/static_assets/hello_world.jinja2 +++ b/docs/quick_tour/static_assets/hello_world.jinja2 @@ -2,7 +2,7 @@ <html lang="en"> <head> <title>Hello World</title> - <link rel="stylesheet" href="/static/app.css"/> + <link rel="stylesheet" href="{{ request.static_url('__main__:static/app.css') }}"/> </head> <body> <h1>Hello {{ name }}!</h1> diff --git a/docs/quick_tour/static_assets/hello_world_static.jinja2 b/docs/quick_tour/static_assets/hello_world_static.jinja2 index 4fb9be074..0fb2ce296 100644 --- a/docs/quick_tour/static_assets/hello_world_static.jinja2 +++ b/docs/quick_tour/static_assets/hello_world_static.jinja2 @@ -2,7 +2,7 @@ <html lang="en"> <head> <title>Hello World</title> - <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/> + <link rel="stylesheet" href="/static/app.css"/> </head> <body> <h1>Hello {{ name }}!</h1> diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py index a8871f6f5..8147052ad 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py @@ -5,8 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .page import Page # flake8: noqa -from .user import User # flake8: noqa +from .page import Page # noqa +from .user import User # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index a8871f6f5..8147052ad 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -5,8 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .page import Page # flake8: noqa -from .user import User # flake8: noqa +from .page import Page # noqa +from .user import User # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py index 48a957ecb..3fc82cfba 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -5,7 +5,7 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa +from .mymodel import MyModel # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py index 48a957ecb..3fc82cfba 100644 --- a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py @@ -5,7 +5,7 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa +from .mymodel import MyModel # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index a8871f6f5..8147052ad 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -5,8 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .page import Page # flake8: noqa -from .user import User # flake8: noqa +from .page import Page # noqa +from .user import User # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py index a8871f6f5..8147052ad 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -5,8 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .page import Page # flake8: noqa -from .user import User # flake8: noqa +from .page import Page # noqa +from .user import User # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index a8871f6f5..8147052ad 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -5,8 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .page import Page # flake8: noqa -from .user import User # flake8: noqa +from .page import Page # noqa +from .user import User # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b13885833..e341922d3 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -899,6 +899,7 @@ class ViewsConfiguratorMixin(object): # __no_permission_required__ handled by _secure_view derived_view = self._derive_view( view, + route_name=route_name, permission=permission, predicates=preds, attr=attr, @@ -1348,7 +1349,7 @@ class ViewsConfiguratorMixin(object): def _derive_view(self, view, permission=None, predicates=(), attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, - phash=DEFAULT_PHASH, decorator=None, + phash=DEFAULT_PHASH, decorator=None, route_name=None, mapper=None, http_cache=None, context=None, require_csrf=None, extra_options=None): view = self.maybe_dotted(view) @@ -1378,6 +1379,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, http_cache=http_cache, require_csrf=require_csrf, + route_name=route_name ) if extra_options: options.update(extra_options) diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py index 4e811a42b..62c3eeecc 100644 --- a/pyramid/scaffolds/__init__.py +++ b/pyramid/scaffolds/__init__.py @@ -35,11 +35,10 @@ class PyramidTemplate(Template): msg = dedent( """ %(separator)s - Tutorials: http://docs.pylonsproject.org/projects/pyramid_tutorials - Documentation: http://docs.pylonsproject.org/projects/pyramid - - Twitter (tips & updates): http://twitter.com/pylons - Mailing List: http://groups.google.com/group/pylons-discuss + Tutorials: http://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/ + Documentation: http://docs.pylonsproject.org/projects/pyramid/en/latest/ + Twitter: https://twitter.com/trypyramid + Mailing List: https://groups.google.com/forum/#!forum/pylons-discuss Welcome to Pyramid. Sorry for the convenience. %(separator)s @@ -53,12 +52,13 @@ class PyramidTemplate(Template): class StarterProjectTemplate(PyramidTemplate): _template_dir = 'starter' - summary = 'Pyramid starter project' + summary = 'Pyramid starter project using URL dispatch and Chameleon' class ZODBProjectTemplate(PyramidTemplate): _template_dir = 'zodb' - summary = 'Pyramid ZODB project using traversal' + summary = 'Pyramid project using ZODB, traversal, and Chameleon' class AlchemyProjectTemplate(PyramidTemplate): _template_dir = 'alchemy' - summary = 'Pyramid SQLAlchemy project using url dispatch' + summary = 'Pyramid project using SQLAlchemy, SQLite, URL dispatch, and' + ' Chameleon' diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl index 26b50aaf6..f626d1ef0 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -5,7 +5,7 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa +from .mymodel import MyModel # noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 74bda1dce..ec7f31704 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -10,8 +10,6 @@ import atexit import ctypes -import errno -import logging import optparse import os import py_compile @@ -59,20 +57,13 @@ def main(argv=sys.argv, quiet=False): command = PServeCommand(argv, quiet=quiet) return command.run() -class DaemonizeException(Exception): - pass - class PServeCommand(object): - usage = '%prog config_uri [start|stop|restart|status] [var=value]' + usage = '%prog config_uri [var=value]' description = """\ This command serves a web application that uses a PasteDeploy configuration file for the server and application. - If start/stop/restart is given, then --daemon is implied, and it will - start (normal operation), stop (--stop-daemon), or do both. - Note: Daemonization features are deprecated. - You can also include variable assignments like 'http_port=8080' and then use %(http_port)s in your config files. """ @@ -98,23 +89,6 @@ class PServeCommand(object): metavar='SECTION_NAME', help=("Use the named server as defined in the configuration file " "(default: main)")) - if hasattr(os, 'fork'): - parser.add_option( - '--daemon', - dest="daemon", - action="store_true", - help="Run in daemon (background) mode [DEPRECATED]") - parser.add_option( - '--pid-file', - dest='pid_file', - metavar='FILENAME', - help=("Save PID to file (default to pyramid.pid if running in " - "daemon mode) [DEPRECATED]")) - parser.add_option( - '--log-file', - dest='log_file', - metavar='LOG_FILE', - help="Save output to the given log file (redirects stdout) [DEPRECATED]") parser.add_option( '--reload', dest='reload', @@ -127,22 +101,11 @@ class PServeCommand(object): help=("Seconds between checking files (low number can cause " "significant CPU usage)")) parser.add_option( - '--monitor-restart', - dest='monitor_restart', - action='store_true', - help="Auto-restart server if it dies [DEPRECATED]") - parser.add_option( '-b', '--browser', dest='browser', action='store_true', help="Open a web browser to server url") parser.add_option( - '--status', - action='store_true', - dest='show_status', - help=("Show the status of the (presumably daemonized) server " - "[DEPRECATED]")) - parser.add_option( '-v', '--verbose', default=default_verbosity, dest='verbose', @@ -155,33 +118,11 @@ class PServeCommand(object): dest='verbose', help="Suppress verbose output") - if hasattr(os, 'setuid'): - # I don't think these are available on Windows - parser.add_option( - '--user', - dest='set_user', - metavar="USERNAME", - help="Set the user (usually only possible when run as root)") - parser.add_option( - '--group', - dest='set_group', - metavar="GROUP", - help="Set the group (usually only possible when run as root)") - - parser.add_option( - '--stop-daemon', - dest='stop_daemon', - action='store_true', - help=('Stop a daemonized server (given a PID file, or default ' - 'pyramid.pid file) [DEPRECATED]')) - _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN' _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN' - possible_subcommands = ('start', 'stop', 'restart', 'status') - def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) if quiet: @@ -192,51 +133,16 @@ class PServeCommand(object): print(msg) def get_options(self): - if ( - len(self.args) > 1 and - self.args[1] in self.possible_subcommands - ): - restvars = self.args[2:] - else: - restvars = self.args[1:] - + restvars = self.args[1:] return parse_vars(restvars) def run(self): # pragma: no cover - if self.options.stop_daemon: - self._warn_daemon_deprecated() - return self.stop_daemon() - - if not hasattr(self.options, 'set_user'): - # Windows case: - self.options.set_user = self.options.set_group = None - - # @@: Is this the right stage to set the user at? - if self.options.set_user or self.options.set_group: - self.change_user_group( - self.options.set_user, self.options.set_group) - if not self.args: self.out('You must give a config file') return 2 app_spec = self.args[0] - if ( - len(self.args) > 1 and - self.args[1] in self.possible_subcommands - ): - cmd = self.args[1] - else: - cmd = None - if self.options.reload: - if ( - getattr(self.options, 'daemon', False) or - cmd in ('start', 'stop', 'restart') - ): - self.out( - 'Error: Cannot use reloading while running as a dameon.') - return 2 if os.environ.get(self._reloader_environ_key): if self.options.verbose > 1: self.out('Running reloading file monitor') @@ -246,31 +152,6 @@ class PServeCommand(object): else: return self.restart_with_reloader() - if cmd not in (None, 'start', 'stop', 'restart', 'status'): - self.out( - 'Error: must give start|stop|restart (not %s)' % cmd) - return 2 - - if cmd == 'status' or self.options.show_status: - self._warn_daemon_deprecated() - return self.show_status() - - if cmd in ('restart', 'stop'): - self._warn_daemon_deprecated() - result = self.stop_daemon() - if result: - if cmd == 'restart': - self.out("Could not stop daemon; aborting") - else: - self.out("Could not stop daemon") - return result - if cmd == 'stop': - return result - self.options.daemon = True - - if cmd == 'start': - self.options.daemon = True - app_name = self.options.app_name vars = self.get_options() @@ -286,75 +167,6 @@ class PServeCommand(object): server_spec = app_spec base = os.getcwd() - # warn before setting a default - if self.options.pid_file or self.options.log_file: - self._warn_daemon_deprecated() - - if getattr(self.options, 'daemon', False): - if not self.options.pid_file: - self.options.pid_file = 'pyramid.pid' - if not self.options.log_file: - self.options.log_file = 'pyramid.log' - - # Ensure the log file is writeable - if self.options.log_file: - try: - writeable_log_file = open(self.options.log_file, 'a') - except IOError as ioe: - msg = 'Error: Unable to write to log file: %s' % ioe - raise ValueError(msg) - writeable_log_file.close() - - # Ensure the pid file is writeable - if self.options.pid_file: - try: - writeable_pid_file = open(self.options.pid_file, 'a') - except IOError as ioe: - msg = 'Error: Unable to write to pid file: %s' % ioe - raise ValueError(msg) - writeable_pid_file.close() - - # warn before forking - if ( - self.options.monitor_restart and - not os.environ.get(self._monitor_environ_key) - ): - self.out('''\ ---monitor-restart has been deprecated in Pyramid 1.6. It will be removed -in a future release per Pyramid's deprecation policy. Please consider using -a real process manager for your processes like Systemd, Circus, or Supervisor. -''') - - if ( - getattr(self.options, 'daemon', False) and - not os.environ.get(self._monitor_environ_key) - ): - self._warn_daemon_deprecated() - try: - self.daemonize() - except DaemonizeException as ex: - if self.options.verbose > 0: - self.out(str(ex)) - return 2 - - if ( - not os.environ.get(self._monitor_environ_key) and - self.options.pid_file - ): - self.record_pid(self.options.pid_file) - - if ( - self.options.monitor_restart and - not os.environ.get(self._monitor_environ_key) - ): - return self.restart_with_monitor() - - if self.options.log_file: - stdout_log = LazyWriter(self.options.log_file, 'a') - sys.stdout = stdout_log - sys.stderr = stdout_log - logging.basicConfig(stream=stdout_log) - log_fn = app_spec if log_fn.startswith('config:'): log_fn = app_spec[len('config:'):] @@ -442,141 +254,6 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. name += '.exe' return name - def daemonize(self): # pragma: no cover - pid = live_pidfile(self.options.pid_file) - if pid: - raise DaemonizeException( - "Daemon is already running (PID: %s from PID file %s)" - % (pid, self.options.pid_file)) - - if self.options.verbose > 0: - self.out('Entering daemon mode') - pid = os.fork() - if pid: - # The forked process also has a handle on resources, so we - # *don't* want proper termination of the process, we just - # want to exit quick (which os._exit() does) - os._exit(0) - # Make this the session leader - os.setsid() - # Fork again for good measure! - pid = os.fork() - if pid: - os._exit(0) - - # @@: Should we set the umask and cwd now? - - import resource # Resource usage information. - maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if (maxfd == resource.RLIM_INFINITY): - maxfd = MAXFD - # Iterate through and close all file descriptors. - for fd in range(0, maxfd): - try: - os.close(fd) - except OSError: # ERROR, fd wasn't open to begin with (ignored) - pass - - if (hasattr(os, "devnull")): - REDIRECT_TO = os.devnull - else: - REDIRECT_TO = "/dev/null" - os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) - # Duplicate standard input to standard output and standard error. - os.dup2(0, 1) # standard output (1) - os.dup2(0, 2) # standard error (2) - - def _remove_pid_file(self, written_pid, filename, verbosity): - current_pid = os.getpid() - if written_pid != current_pid: - # A forked process must be exiting, not the process that - # wrote the PID file - return - if not os.path.exists(filename): - return - with open(filename) as f: - content = f.read().strip() - try: - pid_in_file = int(content) - except ValueError: - pass - else: - if pid_in_file != current_pid: - msg = "PID file %s contains %s, not expected PID %s" - self.out(msg % (filename, pid_in_file, current_pid)) - return - if verbosity > 0: - self.out("Removing PID file %s" % filename) - try: - os.unlink(filename) - return - except OSError as e: - # Record, but don't give traceback - self.out("Cannot remove PID file: (%s)" % e) - # well, at least lets not leave the invalid PID around... - try: - with open(filename, 'w') as f: - f.write('') - except OSError as e: - self.out('Stale PID left in file: %s (%s)' % (filename, e)) - else: - self.out('Stale PID removed') - - def record_pid(self, pid_file): - pid = os.getpid() - if self.options.verbose > 1: - self.out('Writing PID %s to %s' % (pid, pid_file)) - with open(pid_file, 'w') as f: - f.write(str(pid)) - atexit.register(self._remove_pid_file, pid, pid_file, self.options.verbose) - - def stop_daemon(self): # pragma: no cover - pid_file = self.options.pid_file or 'pyramid.pid' - if not os.path.exists(pid_file): - self.out('No PID file exists in %s' % pid_file) - return 1 - pid = read_pidfile(pid_file) - if not pid: - self.out("Not a valid PID file in %s" % pid_file) - return 1 - pid = live_pidfile(pid_file) - if not pid: - self.out("PID in %s is not valid (deleting)" % pid_file) - try: - os.unlink(pid_file) - except (OSError, IOError) as e: - self.out("Could not delete: %s" % e) - return 2 - return 1 - for j in range(10): - if not live_pidfile(pid_file): - break - import signal - kill(pid, signal.SIGTERM) - time.sleep(1) - else: - self.out("failed to kill web process %s" % pid) - return 3 - if os.path.exists(pid_file): - os.unlink(pid_file) - return 0 - - def show_status(self): # pragma: no cover - pid_file = self.options.pid_file or 'pyramid.pid' - if not os.path.exists(pid_file): - self.out('No PID file %s' % pid_file) - return 1 - pid = read_pidfile(pid_file) - if not pid: - self.out('No PID in file %s' % pid_file) - return 1 - pid = live_pidfile(pid_file) - if not pid: - self.out('PID %s in %s is not running' % (pid, pid_file)) - return 1 - self.out('Server running in PID %s' % pid) - return 0 - def restart_with_reloader(self): # pragma: no cover self.restart_with_monitor(reloader=True) @@ -624,60 +301,6 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. if self.options.verbose > 0: self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20)) - def change_user_group(self, user, group): # pragma: no cover - import pwd - import grp - - self.out('''\ -The --user and --group options have been deprecated in Pyramid 1.6. They will -be removed in a future release per Pyramid's deprecation policy. Please -consider using a real process manager for your processes like Systemd, Circus, -or Supervisor, all of which support process security. -''') - - uid = gid = None - if group: - try: - gid = int(group) - group = grp.getgrgid(gid).gr_name - except ValueError: - import grp - try: - entry = grp.getgrnam(group) - except KeyError: - raise ValueError( - "Bad group: %r; no such group exists" % group) - gid = entry.gr_gid - try: - uid = int(user) - user = pwd.getpwuid(uid).pw_name - except ValueError: - try: - entry = pwd.getpwnam(user) - except KeyError: - raise ValueError( - "Bad username: %r; no such user exists" % user) - if not gid: - gid = entry.pw_gid - uid = entry.pw_uid - if self.options.verbose > 0: - self.out('Changing user to %s:%s (%s:%s)' % ( - user, group or '(unknown)', uid, gid)) - if gid: - os.setgid(gid) - if uid: - os.setuid(uid) - - def _warn_daemon_deprecated(self): - self.out('''\ -The daemon options have been deprecated in Pyramid 1.6. They will be removed -in a future release per Pyramid's deprecation policy. Please consider using -a real process manager for your processes like Systemd, Circus, or Supervisor. - -The following commands are deprecated: - [start,stop,restart,status] --daemon, --stop-server, --status, --pid-file, --log-file -''') - class LazyWriter(object): """ @@ -718,33 +341,6 @@ class LazyWriter(object): def flush(self): self.open().flush() -def live_pidfile(pidfile): # pragma: no cover - """(pidfile:str) -> int | None - Returns an int found in the named file, if there is one, - and if there is a running process with that process id. - Return None if no such process exists. - """ - pid = read_pidfile(pidfile) - if pid: - try: - kill(int(pid), 0) - return pid - except OSError as e: - if e.errno == errno.EPERM: - return pid - return None - -def read_pidfile(filename): - if os.path.exists(filename): - try: - with open(filename) as f: - content = f.read() - return int(content.strip()) - except (ValueError, IOError): - return None - else: - return None - def ensure_port_cleanup( bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover """ diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index bc21665aa..bf4763602 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -1,23 +1,11 @@ -import atexit import os import tempfile import unittest -from pyramid.compat import PY2 -if PY2: - import __builtin__ -else: - import builtins as __builtin__ - class TestPServeCommand(unittest.TestCase): def setUp(self): from pyramid.compat import NativeIO self.out_ = NativeIO() - self.pid_file = None - - def tearDown(self): - if self.pid_file and os.path.exists(self.pid_file): - os.remove(self.pid_file) def out(self, msg): self.out_.write(msg) @@ -39,172 +27,12 @@ class TestPServeCommand(unittest.TestCase): cmd.out = self.out return cmd - def _makeOneWithPidFile(self, pid): - self.pid_file = tempfile.mktemp() - inst = self._makeOne() - with open(self.pid_file, 'w') as f: - f.write(str(pid)) - return inst - - def test_remove_pid_file_verbose(self): - inst = self._makeOneWithPidFile(os.getpid()) - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - self._assert_pid_file_removed(verbose=True) - - def test_remove_pid_file_not_verbose(self): - inst = self._makeOneWithPidFile(os.getpid()) - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=0) - self._assert_pid_file_removed(verbose=False) - - def test_remove_pid_not_a_number(self): - inst = self._makeOneWithPidFile('not a number') - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - self._assert_pid_file_removed(verbose=True) - - def test_remove_pid_current_pid_is_not_written_pid(self): - inst = self._makeOneWithPidFile(os.getpid()) - inst._remove_pid_file('99999', self.pid_file, verbosity=1) - self._assert_pid_file_not_removed('') - - def test_remove_pid_current_pid_is_not_pid_in_file(self): - inst = self._makeOneWithPidFile('99999') - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - msg = 'PID file %s contains 99999, not expected PID %s' - self._assert_pid_file_not_removed(msg % (self.pid_file, os.getpid())) - - def test_remove_pid_no_pid_file(self): - inst = self._makeOne() - self.pid_file = 'some unknown path' - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - self._assert_pid_file_removed(verbose=False) - - def test_remove_pid_file_unlink_exception(self): - inst = self._makeOneWithPidFile(os.getpid()) - self._remove_pid_unlink_exception(inst) - msg = [ - 'Removing PID file %s' % (self.pid_file), - 'Cannot remove PID file: (Some OSError - unlink)', - 'Stale PID removed'] - self._assert_pid_file_not_removed(msg=''.join(msg)) - with open(self.pid_file) as f: - self.assertEqual(f.read(), '') - - def test_remove_pid_file_stale_pid_write_exception(self): - inst = self._makeOneWithPidFile(os.getpid()) - self._remove_pid_unlink_and_write_exceptions(inst) - msg = [ - 'Removing PID file %s' % (self.pid_file), - 'Cannot remove PID file: (Some OSError - unlink)', - 'Stale PID left in file: %s ' % (self.pid_file), - '(Some OSError - open)'] - self._assert_pid_file_not_removed(msg=''.join(msg)) - with open(self.pid_file) as f: - self.assertEqual(int(f.read()), os.getpid()) - - def test_record_pid_verbose(self): - self._assert_record_pid(verbosity=2, msg='Writing PID %d to %s') - - def test_record_pid_not_verbose(self): - self._assert_record_pid(verbosity=1, msg='') - - def _remove_pid_unlink_exception(self, inst): - old_unlink = os.unlink - def fake_unlink(filename): - raise OSError('Some OSError - unlink') - - try: - os.unlink = fake_unlink - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - finally: - os.unlink = old_unlink - - def _remove_pid_unlink_and_write_exceptions(self, inst): - old_unlink = os.unlink - def fake_unlink(filename): - raise OSError('Some OSError - unlink') - - run_already = [] - old_open = __builtin__.open - def fake_open(*args): - if not run_already: - run_already.append(True) - return old_open(*args) - raise OSError('Some OSError - open') - - try: - os.unlink = fake_unlink - __builtin__.open = fake_open - inst._remove_pid_file(os.getpid(), self.pid_file, verbosity=1) - finally: - os.unlink = old_unlink - __builtin__.open = old_open - - def _assert_pid_file_removed(self, verbose=False): - self.assertFalse(os.path.exists(self.pid_file)) - msg = 'Removing PID file %s' % (self.pid_file) if verbose else '' - self.assertEqual(self.out_.getvalue(), msg) - - def _assert_pid_file_not_removed(self, msg): - self.assertTrue(os.path.exists(self.pid_file)) - self.assertEqual(self.out_.getvalue(), msg) - - def _assert_record_pid(self, verbosity, msg): - old_atexit = atexit.register - def fake_atexit(*args): - pass - - self.pid_file = tempfile.mktemp() - pid = os.getpid() - inst = self._makeOne() - inst.options.verbose = verbosity - - try: - atexit.register = fake_atexit - inst.record_pid(self.pid_file) - finally: - atexit.register = old_atexit - - msg = msg % (pid, self.pid_file) if msg else '' - self.assertEqual(self.out_.getvalue(), msg) - with open(self.pid_file) as f: - self.assertEqual(int(f.read()), pid) - def test_run_no_args(self): inst = self._makeOne() result = inst.run() self.assertEqual(result, 2) self.assertEqual(self.out_.getvalue(), 'You must give a config file') - def test_run_stop_daemon_no_such_pid_file(self): - path = os.path.join(os.path.dirname(__file__), 'wontexist.pid') - inst = self._makeOne('--stop-daemon', '--pid-file=%s' % path) - inst.run() - msg = 'No PID file exists in %s' % path - self.assertTrue(msg in self.out_.getvalue()) - - def test_run_stop_daemon_bad_pid_file(self): - path = __file__ - inst = self._makeOne('--stop-daemon', '--pid-file=%s' % path) - inst.run() - msg = 'Not a valid PID file in %s' % path - self.assertTrue(msg in self.out_.getvalue()) - - def test_run_stop_daemon_invalid_pid_in_file(self): - fn = tempfile.mktemp() - with open(fn, 'wb') as tmp: - tmp.write(b'9999999') - tmp.close() - inst = self._makeOne('--stop-daemon', '--pid-file=%s' % fn) - inst.run() - msg = 'PID in %s is not valid (deleting)' % fn - self.assertTrue(msg in self.out_.getvalue()) - - def test_get_options_with_command(self): - inst = self._makeOne() - inst.args = ['foo', 'stop', 'a=1', 'b=2'] - result = inst.get_options() - self.assertEqual(result, {'a': '1', 'b': '2'}) - def test_get_options_no_command(self): inst = self._makeOne() inst.args = ['foo', 'a=1', 'b=2'] @@ -233,29 +61,6 @@ class TestPServeCommand(unittest.TestCase): inst.loadserver = self._get_server self.assertRaises(ValueError, inst.run) -class Test_read_pidfile(unittest.TestCase): - def _callFUT(self, filename): - from pyramid.scripts.pserve import read_pidfile - return read_pidfile(filename) - - def test_read_pidfile(self): - filename = tempfile.mktemp() - try: - with open(filename, 'w') as f: - f.write('12345') - result = self._callFUT(filename) - self.assertEqual(result, 12345) - finally: - os.remove(filename) - - def test_read_pidfile_no_pid_file(self): - result = self._callFUT('some unknown path') - self.assertEqual(result, None) - - def test_read_pidfile_not_a_number(self): - result = self._callFUT(__file__) - self.assertEqual(result, None) - class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.pserve import main |
