diff options
Diffstat (limited to 'docs/tutorials/wiki2/basiclayout.rst')
| -rw-r--r-- | docs/tutorials/wiki2/basiclayout.rst | 481 |
1 files changed, 315 insertions, 166 deletions
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 6151e0e25..ce67bb9e3 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -1,189 +1,338 @@ +.. _wiki2_basic_layout: + ============ 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. - -The generated ``development.ini`` file is read by ``paster`` which looks for -the application module in the ``use`` variable of the ``app:tutorial`` -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``. - -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:tutorial]`` section. This will be a URI (something like -``sqlite://``): - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 6-9 - :linenos: - :language: py - -We then initialize 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`: - - .. 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 ``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): - - .. 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. - -Using the configurator we can also register a :term:`route configuration` -via the :meth:`pyramid.config.Configurator.add_route` method that will be -used when the URL is ``/``: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :language: py - -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: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14-15 - :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: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 16 - :language: py - -Our final ``__init__.py`` file will look like this: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :linenos: - :language: py - -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. - -Let's take a look. First, we need some imports to support later code. - - .. literalinclude:: src/basiclayout/tutorial/models.py - :end-before: DBSession - :linenos: - :language: py +package. We use ``__init__.py`` both as a marker, indicating the directory in +which it's contained is a package, and to contain application configuration +code. -Next we set up a SQLAlchemy "DBSession" object: +Open ``tutorial/__init__.py``. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/models.py - :lines: 15-16 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :linenos: + :language: py -We also need to create a declarative ``Base`` object to use as a -base class for our model: +Let's go over this piece-by-piece. First we need some imports to support later +code: - .. literalinclude:: src/basiclayout/tutorial/models.py - :lines: 17 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :end-before: main + :linenos: + :lineno-match: + :language: py + +``__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: + :lineno-match: + :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. (See :ref:`startup_chapter` for more about ``pserve``.) + +Next in ``main``, construct a :term:`Configurator` object: + +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 7 + :lineno-match: + :language: py -To give a simple example of a model class, we define one named ``MyModel``: +``settings`` is passed to the ``Configurator`` as a keyword argument with the +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``, +``sqlalchemy.url``, and so on. + +Next include :term:`Jinja2` templating bindings so that we can use renderers +with the ``.jinja2`` extension within our project. + +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 8 + :lineno-match: + :language: py + +Next include the the package ``models`` using a dotted Python path. The exact +setup of the models will be covered later. + +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 9 + :lineno-match: + :language: py + +Next include the ``routes`` module using a dotted Python path. This module will +be explained in the next section. + +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 10 + :lineno-match: + :language: py + +.. note:: + + Pyramid's :meth:`pyramid.config.Configurator.include` method is the primary + mechanism for extending the configurator and breaking your code into + feature-focused modules. - .. literalinclude:: src/basiclayout/tutorial/models.py - :pyobject: MyModel - :linenos: - :language: py +``main`` next calls the ``scan`` method of the configurator +(:meth:`pyramid.config.Configurator.scan`), 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, allowing one of our application URLs to be mapped to some +code. -Our sample model has an ``__init__`` that takes a two arguments (``name``, -and ``value``). It stores these values as ``self.name`` and ``self.value`` -within the ``__init__`` function itself. The ``MyModel`` class also has a -``__tablename__`` attribute. This informs SQLAlchemy which table to use to -store the data representing instances of this class. +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 11 + :lineno-match: + :language: py -Next we define a function named ``populate`` which adds a single -model instance into our SQL storage and commits a transaction: +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/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. +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 12 + :lineno-match: + :language: py + + +Route declarations +------------------ + +Open the ``tutorials/routes.py`` file. It should already contain the following: + +.. literalinclude:: src/basiclayout/tutorial/routes.py + :linenos: + :language: py + +On line 2, we call :meth:`pyramid.config.Configurator.add_static_view` with +three arguments: ``static`` (the name), ``static`` (the path), and +``cache_max_age`` (a keyword argument). + +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. + +On line 3, the module registers a :term:`route configuration` via the +:meth:`pyramid.config.Configurator.add_route` method that will be used when the +URL is ``/``. Since this route has a ``pattern`` equaling ``/``, it is the +route that will be matched when the URL ``/`` is visited, e.g., +``http://localhost:6543/``. + + +View declarations via the ``views`` package +------------------------------------------- + +The main function of a web framework is mapping each URL pattern to code (a +:term:`view callable`) that is executed when the requested URL matches the +corresponding :term:`route`. Our application uses the +:meth:`pyramid.view.view_config` decorator to perform this mapping. + +Open ``tutorial/views/default.py`` in the ``views`` package. It should already +contain the following: -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/views/default.py + :linenos: + :language: py - .. literalinclude:: src/basiclayout/tutorial/models.py - :pyobject: initialize_sql - :linenos: - :language: py +The important part here is that the ``@view_config`` decorator associates the +function it decorates (``my_view``) with a :term:`view configuration`, +consisting of: -Here is the complete source for ``models.py``: + * a ``route_name`` (``home``) + * a ``renderer``, which is a template from the ``templates`` subdirectory of + the package. - .. literalinclude:: src/basiclayout/tutorial/models.py - :linenos: - :language: py +When the pattern associated with the ``home`` view is matched during a request, +``my_view()`` will be executed. ``my_view()`` returns a dictionary; the +renderer will use the ``templates/mytemplate.jinja2`` template to create a +response based on the values in the dictionary. +Note that ``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 ``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`. + +The sample ``my_view()`` created by the scaffold uses a ``try:`` and +``except:`` clause to detect if there is a problem accessing the project +database and provide an alternate error response. That response will include +the text shown at the end of the file, which will be displayed in the browser +to inform the user about possible actions to take to solve the problem. + + +Content models with the ``models`` package +------------------------------------------ + +In an SQLAlchemy-based application, a *model* object is an object composed by +querying the SQL database. The ``models`` package is where the ``alchemy`` +scaffold put the classes that implement our models. + +First, open ``tutorial/models/meta.py``, which should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :linenos: + :language: py + +``meta.py`` contains imports and support code for defining the models. We +create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of +support objects like indices and constraints. + +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :end-before: metadata + :linenos: + :language: py + +Next we create a ``metadata`` object from the class +:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value +for the ``naming_convention`` argument. + +A ``MetaData`` object represents the table and other schema definitions for a +single database. We also need to create a declarative ``Base`` object to use as +a base class for our models. Our models will inherit from this ``Base``, which +will attach the tables to the ``metadata`` we created, and define our +application's database schema. + +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :lines: 15-16 + :lineno-match: + :linenos: + :language: py + +Next open ``tutorial/models/mymodel.py``, which should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :linenos: + :language: py + +Notice we've defined the ``models`` as a package to make it straightforward for +defining models in separate modules. To give a simple example of a model class, +we have defined one named ``MyModel`` in ``mymodel.py``: + +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :pyobject: MyModel + :lineno-match: + :linenos: + :language: py + +Our example model does not require an ``__init__`` method because SQLAlchemy +supplies for us a default constructor, if one is not already present, which +accepts keyword arguments of the same name as that of the mapped attributes. + +.. note:: Example usage of MyModel: + + .. code-block:: python + + johnny = MyModel(name="John Doe", value=10) + +The ``MyModel`` class has a ``__tablename__`` attribute. This informs +SQLAlchemy which table to use to store the data representing instances of this +class. + +Finally, open ``tutorial/models/__init__.py``, which should already +contain the following: + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :linenos: + :language: py + +Our ``models/__init__.py`` module defines the primary API we will use for +configuring the database connections within our application, and it contains +several functions we will cover below. + +As we mentioned above, the purpose of the ``models.meta.metadata`` object is to +describe the schema of the database. This is done by defining models that +inherit from the ``Base`` object attached to that ``metadata`` object. In +Python, code is only executed if it is imported, and so to attach the +``models`` table defined in ``mymodel.py`` to the ``metadata``, we must import +it. If we skip this step, then later, when we run +:meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created +because the ``metadata`` object does not know about it! + +Another important reason to import all of the models is that, when defining +relationships between models, they must all exist in order for SQLAlchemy to +find and build those internal mappings. This is why, after importing all the +models, we explicitly execute the function +:func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have +been defined and before we start creating connections. + +Next we define several functions for connecting to our database. The first and +lowest level is the ``get_engine`` function. This creates an :term:`SQLAlchemy` +database engine using :func:`sqlalchemy.engine_from_config` from the +``sqlalchemy.``-prefixed settings in the ``development.ini`` file's +``[app:main]`` section. This setting is a URI (something like ``sqlite://``). + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_engine + :lineno-match: + :linenos: + :language: py + +The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database +engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class +:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then +used for creating sessions bound to the database engine. + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_session_factory + :lineno-match: + :linenos: + :language: py + +The function ``get_tm_session`` registers a database session with a transaction +manager, and returns a ``dbsession`` object. With the transaction manager, our +application will automatically issue a transaction commit after every request, +unless an exception is raised, in which case the transaction will be aborted. + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_tm_session + :lineno-match: + :linenos: + :language: py + +Finally, we define an ``includeme`` function, which is a hook for use with +:meth:`pyramid.config.Configurator.include` to activate code in a Pyramid +application add-on. It is the code that is executed above when we ran +``config.include('.models')`` in our application's ``main`` function. This +function will take the settings from the application, create an engine, and +define a ``request.dbsession`` property, which we can use to do work on behalf +of an incoming request to our application. + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: includeme + :lineno-match: + :linenos: + :language: py + +That's about all there is to it regarding models, views, and initialization +code in our stock application. + +The ``Index`` import and the ``Index`` object creation in ``mymodel.py`` is +not required for this tutorial, and will be removed in the next step. |
