Getting Started With Pyramid

Create a new directory called expense-tracker. Make sure it’s easily located; we’ll be using this for the next two weeks.

Within this new directory, create and activate a virtual environment. It’s probably easiest to maintain if you create your environment as a directory called ENV inside of your expense-tracker directory.

bash-3.2$ python3 -m venv ENV
bash-3.2$ source ENV/bin/activate
(ENV) bash-3.2$

pip install the most recent versions of pip, setuptools, and ipython.

(ENV) bash-3.2$ pip install -U pip setuptools
(ENV) bash-3.2$ pip install ipython

In the root level of your directory, create .gitignore, README.md, and LICENSE files.

  • Get a good Python .gitignore file from GitHub. If you Google for “Python gitignore”, the first result should be a fine one. Because we mostly use Macs in this class, add a line to ignore any .DS_Store files.
  • An MIT license should work, as this is an open source project that should be easily used and improved upon by anyone.
  • Your README should include the name of the project (Expense Tracker), the author (you), and a description of what it is, how to install it, and how it works. You can fill those out (and update them) as the week goes on, but it should be accurate with whatever work has been done to that point.

Initialize a repository, in this directory, and we can get to work.

Installation

In order to begin working with Pyramid, we have to install it. We’ll also install the extension that allows Pyramid to interact with iPython, so that we get a nice-looking interpreter instead of the default Python one:

(ENV) bash-3.2$ pip install pyramid pyramid_ipython

The version that should be pulled down is the latest version, 1.8. Note the other packages that get installed along with it, as it has dependencies. For example, WebOb provides an HTTP Request and Response class, and those you work with in Pyramid inherit from them. Many other frameworks also use this package.

When it is installed, Pyramid creates a bunch of new shell commands (pcreate, pshell, prequest, etc). You can see them all in the bin directory of your virtual environment.

(ENV) bash-3.2$ ls ENV/bin
activate         hupper           pcreate          prequest         pviews
activate.csh     iptest           pdistreport      proutes          pygmentize
activate.fish    iptest3          pip              pserve           python
easy_install     ipython          pip3             pshell           python3
easy_install-3.6 ipython3         pip3.6           ptweens

Writing a “Hello World” App

Source: trypyramid.com.

As is tradition, when using a new bit of technology we test that it works by having it print something like “Hello World”. This is no different. Make a directory for your “hello world” app called hello_world. Within that directory create a file named app.py and type the following:

"""Simple Hello World! app."""
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response


def hello_world(request):
    """Basic view for the hello_world application."""
    return Response("Hello World!")

if __name__ == '__main__':
    config = Configurator()
    config.add_route('hello', '/')
    config.add_view(hello_world, route_name='hello')
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 6543, app)
    server.serve_forever()

Save that file and run the following from the command line:

(ENV) bash-3.2$ python app.py

Notice how the shell returns nothing. This is a good thing. That means that the server you’ve set up through Pyramid is running and listening for requests.

Finally, open http://localhost:6543/ in your browser. This will simply connect you to the port that you told Pyramid to listen to, at 6543.

This is an almost irresponsibly-simple web application. It proves that the Pyramid framework can handle HTTP requests and generate HTTP responses. We’ll definitely be using Pyramid for significantly more-complex things. You can see that it is easy to get a simple site up and running with Pyramid. For the more complex stuff, it helps to have some structure set up beforehand.

Navigate back up one directory, and delete hello_world so that we can continue fresh.

Using cookiecutter to Create a Scaffold

A scaffold is a skeleton of code for a web app. This skeleton includes the basic functionality and reflects the best practices involved in build a Pyramid application.

In the past, this scaffold was generated by the pcreate shell command that is installed in your environment when you pip install the Pyramid package. As of Pyramid 1.8, we’ve moved away from pcreate and now use the cookiecutter package. It accomplishes the same goal, but is more efficient than what came prepackaged with Pyramid. We’ve gotta go through a couple extra steps though to get it.

(ENV) bash-3.2$ pip install cookiecutter
(ENV) bash-3.2$ cookiecutter https://github.com/Pylons/pyramid-cookiecutter-alchemy
project_name [Pyramid Scaffold]:

You’ll be hit with a few prompts asking you about how you want your scaffold configured. Here’s how we’ll handle it for the expense-tracker app we’re building:

  • project_name: Expense Tracker
  • repo_name: expense_tracker

Once you’ve set these, cookiecutter will create a directory called expense_tracker inside of your current expense-tracker directory. Let’s be clear here about where things are located, now that we have a bit of a nested situation:

  • expense-tracker is the directory holding everything. It contains your README.md, your LICENSE, your .gitignore, the actual location of your .git repository, and your ENV directory that houses your virtual environment
  • expense-tracker/expense_tracker is your project root. It holds your project configuration files (ending in .ini), your setup.py, and your expense_tracker application.
  • expense-tracker/expense_tracker/expense_tracker is your application root. It holds the files that make your application actually run.

As a part of the scaffold, cookiecutter assumes that we’ll be creating this project as its own directory, with Pyramid installed globally on our machines. As a part of that, it assumes that we’ll be initializing a git repository at the level of the project root. That is not the case. As such, we can delete the .gitignore file that cookiecutter provided for us. We’ve got our own.

Add this entire directory to your repository with git add .. Commit it, as we’ve got the setup for our first project.

The project root directory contains a bunch of files. They contain packaging metadata, information for other developers and configuration instructions for our application: Type tree -L 1 so you’ll see all of the new files that were just created in this directory (without seeing the full depth of directory tree).

(ENV) bash-3.2$ tree -L 1

├── CHANGES.txt - here, we can track what changes we make to our app over time
├── MANIFEST.in - controls what files are actually present when we package our stuff together and upload it
├── README.txt - is our README file. If you prefer one in Markdown, edit setup.py accordingly
├── development.ini - discussed later
├── production.ini - discussed later
├── pytest.ini - directs ``pytest`` as to which files to test (presuming any file ending in "``.py``")
├── setup.py - lets our directory become an installable python package
├── .coveragerc - determines which directories get targeted for reports of coverage with testing

Let’s start by inspecting setup.py. We can see that this app requires Pyramid, Jinja2 (a templating engine), and a few other packages to work that we’ll get to later on this week. It also comes packed ready to install some packages for tests.

Let’s modify setup.py so that it runs with tox as part of its test suite, and also includes ipython and pyramid_ipython as a part of the installation process:

# in setup.py
requires = [
    'pyramid',
    'pyramid_jinja2',
    'pyramid_debugtoolbar',
    'pyramid_tm',
    'ipython', # <---- add this line
    'pyramid_ipython', # <---- add this line
    'SQLAlchemy',
    'transaction',
    'zope.sqlalchemy',
    'waitress',
]

...
tests_require = [
    'WebTest >= 1.3.1',  # py3 compat
    'pytest',  # includes virtualenv
    'pytest-cov',
    'tox', # you have to add this one in
]
...
setup(name='expense_tracker',
    version='0.0',
    ... # package metadata
    install_requires=requires,
    entry_points={
        "paste.app_factory": [
            'main = expense_tracker:main',
        ],
        "console_scripts": [
            'initialize_expense_tracker_db = expense_tracker.scripts.initializedb:main',
        ]
    }
)

Don’t forget to fill in the appropriate information about author, author_email, etc. Now, let’s install it in editing mode so that the changes we make to this project will be immediately available to us when running the app.

(ENV) bash-3.2$ pip install -e .

One of the things produced after installing our package is an *.egg-info directory. This file is package metadata that should never be in version control. Thankfully, our super .gitignore from GitHub includes this, so we don’t have to worry about it.

Pyramid is Python

Navigate to the expense_tracker application in your project root and inspect it.

(ENV) bash-3.2$ ls
__init__.py models      routes.py   scripts     static      templates   tests.py    views

In the __init__.py file you’ll find a main function. This function is the “entry point” for our application. You can find it registered in setup.py as a paste.app_factory entry point. When you use pserve to start a web server serving your app, this function is executed.

from pyramid.config import Configurator


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.include('pyramid_jinja2')
    config.include('.models')
    config.include('.routes')
    config.scan()
    return config.make_wsgi_app()

This looks somewhat different from the app.py file we had created earlier. The machinery of our framework is now handling some of the stuff we hard coded before. Let’s look at this in detail.

def main(global_config, **settings):

Configuration is passed into an application after being read from the specified .ini file (e.g. development.ini). The settings come in through, you guessed it, the **settings kwarg. The .ini files contain sections (e.g. [app:main]) containing name = value pairs of configuration data. This data is parsed with the Python ConfigParser module, which reads the configuration data and returns it as a dictionary.

The name-value pairs in the [app:main] section of the configuration file are passed in to our app as settings. All other information in the configuration file is passed as global_config. In the context of our main function, settings is a Python dictionary:

{'pyramid.debug_notfound': 'false',
'pyramid.reload_templates': 'true',
'pyramid.default_locale_name': 'en',
...
}

Those settings are used on the next line after the docstring:

config = Configurator(settings=settings)

Here, a Configurator class object is instantiated using the settings for our specific app.

We can also include configuration from other add-on packages and even other regions of the app we’re inside of. This allows for including plugin code that changes how Pyramid behaves. Our app includes configuration from the package we will want to use for templating:

config.include('pyramid_jinja2')

The next bit would include the models directory in the app configuration.

config.include('.models')

However, as we’re not using models for a couple days, we can safely comment this line out.

Following the models, we include routes.py.

config.include('.routes')

This includes all of the URL paths that we want accessible with our application, split out line-by-line. It also includes where our static files (e.g. our HTML, CSS, and JavaScript files if we have any) will reside.

The last bit is

config.scan()
return config.make_wsgi_app()

config.scan() finds all configuration and checks it to make sure that there are no problems with how everything is wired together. Calling config.make_wsgi_app() builds your Pyramid application and returns it to the framework to be served.

We’ll return to the configuration of our application repeatedly over the next few sessions. For greater detail about configuration in Pyramid, check the configuration chapter of the Pyramid documentation.

Routes and The MVC Controller

Let’s go back to thinking for a bit about the Model-View-Controller pattern.

By Alan Evangelista (Own work) [CCo]

By Alan Evangelista (Own work) [CCo], via Wikimedia Commons

HTTP Request/Response

Recall the HTTP server that we built last week. It shows how internet software is driven by the HTTP Request/Response cycle. A client (perhaps a user with a web browser) makes a request. A server receives and handles that request and returns a response. The client receives the response and views it, perhaps making a new request, and so on and so forth.

An HTTP request arrives at a server through the magic of a URL

http://www.codefellows.org/courses/code-401/advanced-software-development-in-python

Let’s break that up into its constituent parts:

http://:
This part is the protocol, it determines how the request will be sent.
www.codefellows.org:
This is a domain name. It’s the human-facing address for a server somewhere.
/courses/code-401/advanced-software-development-in-python:
This part is the path. It serves as a locator for a resource on the server.

In a static website the path identifies a physical location in the server’s file system. Some directory on the server is the home for the web process, and the path is looked up relative to that. Whatever resource (a file, an image, whatever) is located there is returned to the user as a response. If the path leads to a location that doesn’t exist, the server responds with a 404 Not Found error.

In the golden days of yore, this was the only way content was served via HTTP. In today’s world we have dynamic systems, employing server-side web frameworks like Pyramid. The requests that you send to a server are handled by a software process that assembles a response instead of looking up a physical location. But, we still have URLs, with protocol, domain, and path. What is the role for a path in a process that doesn’t refer to a physical file system?

Routes in Pyramid

Most web frameworks now call the path a route, and provide a way of matching routes to the code that will be run to handle requests. This process is called “dispatch”.

In our Pyramid scaffold, routes are handled as configuration. They can be configured in the main function in __init__.py by writing a line that adds a route to the base Configurator instance:

# back inside __init__.py
def main(global_config, **settings):
    #...
    config.add_route('home', '/')
    #...

The add_route method takes a required name argument for each route added. Everything else is, to some degree, an optional argument. Above, we also provide the pattern that gets appended to the site’s root URL (in this case, “/”). Anything that we use accessing the specified name argument in our Pyramid app will be broadcast to the pattern that we provide.

When a request comes in to a Pyramid application, the framework looks at all the routes that have been configured. One by one, in order, it tries to match the path of the incoming request against the pattern of the route. As soon as a pattern matches the path from the incoming request, that route is used and no further matching is performed. If no route is found that matches, then the framework will automatically generate a 404 Not Found error response.

In a very real sense, the routes defined in an application are the public API. Any route that is present represents something the user can do. Any route that is not present is something the user cannot do.

One can imagine that if we were to build a site with many routes (as we will), it would clutter up this main function, causing it to really be handling multiple things instead of being singularly focused (as functions should be). As a completely hypothetical example:

# a hypothetical __init__.py; DO NOT TYPE THIS

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.include('pyramid_jinja2')
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_static_view('special_styles', 'special_styles', cache_max_age=3600)
    config.add_static_view('misc_styles', 'misc_styles', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('about', '/about-me')
    config.add_route('create', '/journal/new-entry')
    config.add_route('edit', '/journal/edit-entry')
    config.add_route('delete', '/journal/delete-entry')
    config.add_route('view', '/journal/{id:\d+}')
    config.add_route('contact', '/contact-me')
    config.add_route('register', '/register')
    config.add_route('login', '/login')
    config.add_route('logout', '/logout')
    config.add_route('settings', '/settings')
    config.scan()
    return config.make_wsgi_app()

Luckily, we can break out our routes and our static views into a routes.py file in the same directory. The sole purpose of this file will be to handle all of the routing configuration for our Pyramid site. The configurator’s include() method will look for a function called includeme, within which we define all of our routes:

# inside routes.py
def includeme(config):
    """ This function adds routes to Pyramid's Configurator """
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    # ...

The first line establishes a directory to hold our static files (css, javascript, images, etc).

config.add_static_view("static", "static", cache_max_age=3600)

The add_static_view method takes two arguments. The first is a path to the directory you will use to hold static files, relative to the location of this __init__.py file. The second is an initial path segment to be used in URLs. The latter is used when Pyramid is automatically generating URLs for static files to be served.

That first one adds a path to your URL of <whatever your domain name is>/. The .add_route() method adds a “route name” to your Pyramid site. Route names are used to connect the URLs that a client requests to something that produces HTML. Here, when a client requests <whatever your domain name is>/, the route named home will be found. That name can be used to find some HTML to return. If instead the second argument was '/new_entry', then requesting <whatever your domain name is>/new_entry would find the home route.

The config.add_route line actually adds a new “path” to our route configuration, with the name of home. Later on when we connect “view” functions to these routes, we’ll use this name and not worry about what the actual path is, because it’ll be mapped to the name we set.

Let’s take a look back at the main function in __init__.py:

# inside __init__.py
def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.include('pyramid_jinja2')
    # config.include('.models')
    config.include('.routes')
    config.scan()
    return config.make_wsgi_app()

Note

The name includeme for a function that takes a Configurator instance is not just convention. This is an example of “magic”. If you provide a dotted path to a Python module or script to include, that module must provide a function called includeme.

We have our routes, and so anything we connect to specific route names will be shown on the associated pages. However, we do not yet have anything (of substance) to show on those pages. We can change all that with Views.

The Pyramid View

Let’s imagine that a request has come to our application for the path '/'. The framework made a match of that path to a route with the pattern '/'. Configuration can connect that route to a view in our application. Then the view that is connected will be called.

This brings us to the nature of views.

Note

A Pyramid View is a callable that takes request as an argument.

The view can use information from that request to build appropriate data, perhaps using the application’s models (more on that tomorrow). Finally, it returns the data it assembled.

If you recall our hello_world app, we defined a function named hello_world(). It took a request as an argument and used Pyramid’s Response object to provide an HTTP response. If we look inside of the views/default.py file provided by the Alchemy scaffold, we’ll find something similar.

# views.py
from pyramid.response import Response
from pyramid.view import view_config

from sqlalchemy.exc import DBAPIError

from ..models import MyModel


@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
    query = request.dbsession.query(MyModel)
    one = query.filter(MyModel.name == 'one').first()
except DBAPIError:
    return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'Expense Tracker'}

Here, my_view is the function name, taking a request, and a dictionary is being returned as a response. This is great and all, but let’s start more simply. Delete everything in the file and replace it with the following:

"""Views for the Expense Tracker app."""
from pyramid.response import Response


def home_page(request):
    """View for the home route."""
    return Response("This is my first view!")

Then, in the __init__.py file in the views directory write

from .default import home_page


def includeme(config):
    """List of views to include for the configurator object."""
    config.add_view(home_page, route_name='home')

In the includeme function in this module, we connect this view to our existing home route. The add_view method takes the name of a view callable and the name of a route as arguments.

Finally, we can include this configuration in our main function in __init__.py:

# __init__.py

#...
def main(global_config, **settings):
    # ...
    config.include('.views') <-- connects our views
    config.scan()
    return config.make_wsgi_app()

Again, this says “look in the views module for a function called includeme and include any additions to the overall app configuration.”

Note, it doesn’t matter what order you pull in the config.include statements.

Now that we’re all wired together, let’s navigate back to our project root and pip install this Pyramid app. Do you remember how to do that? Then, we can use pserve development.ini to start up a server and investigate the fruits of our labor.

What happens if instead of providing a simple string as an argument to Response, we try to include the text contained within another file? Let’s set ourselves up for it by creating a file with that text.

In the same directory as default.py create a file called sample.txt. Within that file, stick some text.

(ENV) bash-3.2$ echo "This is text in an external file." > sample.txt

Now modify the view that we’ve made to read this file into Python, and return that text in the HTTP Response object.

# views.py
# ...
import io # for backwards compatability
import os

HERE = os.path.dirname(__file__)

def home_page(request):
    imported_text = io.open(os.path.join(HERE, 'sample.txt')).read()
    return Response(imported_text)
# ...

Check the browser to see the result.

We don’t just have to work with plain text. Let’s make a new file that contains HTML instead.

(ENV) bash-3.2$ echo "<h1>This is text in an external file.</h1>" > sample.html

And now modify our view to access this new file

# views.py
# ...
import io
import os

HERE = os.path.dirname(__file__)

def home_page(request):
    imported_text = io.open(os.path.join(HERE, 'sample.html')).read()
    return Response(imported_text)
# ...

Re-launch the server and voila, html appears!

Testing Your Pyramid App

THIS WILL NOT BE COMPLETE, JUST AN EXAMPLE OF A COUPLE TESTS

Thus far we have written a bit of code for handling HTTP routing and serving data. But we haven’t yet written any tests of our own to make sure that things work the way that we need them to work. We can fortunately do this with Pyramid’s own testing module (Documentation).

When it comes to testing your Pyramid app (as well as other apps in general), you need to not only perform unit tests for individual pieces of functionality. You also need to test for how things perform when in practice. For example, if your app sends an email, you need to check that the email is actually sent.

However, for today, we’ll focus only on unit tests, refactoring and building out more about functional tests tomorrow.

Setting Up a Test for a View

Our scaffold provided for us a tests.py file complete with some basic tests. However, since we won’t be using unittest for our test suite we’ll gut it completely. In its place, write:

# in tests.py

from pyramid import testing
from pyramid.response import Response


def test_home_view_returns_response():
    """Home view returns a Response object."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert isinstance(response, Response)

def test_home_view_is_good():
    """Home view response has a status 200 OK."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert response.status_code == 200

def test_home_view_returns_proper_content():
    """Home view response has file content."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert "This is text in an external file" in response.text

As part of the setup, we have Pyramid’s own testing module. This module provides tools to set up the configuration we need for our app. It also gives access to the request and response objects that we need to test our views. Recall that our views must be called with a request as an argument. Depending on what work your views must do, this can be any Python object, even a simple string or dict. But if you require something with a bit more resemblance to a real request, you can use testing.DummyRequest.

Recall that our home_page view returns a Response object filled with some text. We make our tests reflect that. First we check that what was returned by our view callable is a Response object.

# tests.py
from pyramid import testing
from pyramid.response import Response


def test_home_view_returns_response():
    """Home view returns a Response object."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert isinstance(response, Response)

Then we check that the request was processed properly, returning a status 200.

def test_home_view_is_good():
    """Home view response has a status 200 OK."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert response.status_code == 200

Finally, we test that the actual content of the response matches what we expect.

def test_home_view_returns_proper_content():
    """Home view response has file content."""
    from expense_tracker.views.default import home_page
    request = testing.DummyRequest()
    response = home_page(request)
    assert "This is text in an external file" in response.text

Running Pyramid Tests

To run these tests we have to first install all the packages that we need for testing. We defined those in our setup.py so just navigate to the project root and install like so:

(ENV) bash-3.2$ pip install -e .[testing]

We have .[testing] because we want to install everything in the current directory (the .), but we also want the extra packages that we specified for testing. If you have other extra packages you want for some other reason, you install them in this fashion.

Now that all is installed, run our tests!

(ENV) bash-3.2$ py.test expense_tracker -v

The -v flag makes your test output verbose, telling you the name of every test that passes/fails. For less verbose output, use -q.

We’ve designed these two tests to pass, so we should get a statement saying they pass.

Spectacular.

But we want to test across versions of Python, so we need to incorporate tox. Recall that when we first set up our app via the scaffold, we added tox into tests_require. When we pip-installed testing above, tox was installed along with everything else. Now we just have to construct our tox.ini configuration file so that we can run tox.

Let’s add a little bit more to our tox file than we usually do. We don’t want to just run our tests across versions, we ultimately want to make sure that our app is well-tested across everything we’ve written. We want to add coverage. So, our tox file should look like the following:

[tox]
envlist = py27, py36

[testenv]
commands = py.test --cov=expense_tracker -q
deps =
    pytest
    pytest-cov
    webtest

Now we run tox as we always have and ensure that our test passes across Python 2.7 and 3.6. On top of that, we get a report of the coverage of our tests in the console.

------ coverage: platform darwin, python 3.6.1-final-0 -------
Name                                      Stmts   Miss  Cover
--------------------------------------------------------------
expense_tracker/__init__.py                   8      6    25%
expense_tracker/data/__init__.py              0      0   100%
expense_tracker/data/expense_data.py              1      0   100%
expense_tracker/models/__init__.py           22     12    45%
expense_tracker/models/meta.py                5      0   100%
expense_tracker/models/mymodel.py             8      0   100%
expense_tracker/routes.py                     4      3    25%
expense_tracker/scripts/__init__.py           0      0   100%
expense_tracker/scripts/initializedb.py      26     16    38%
expense_tracker/views/__init__.py             3      1    67%
expense_tracker/views/default.py              7      0   100%
expense_tracker/views/notfound.py             4      2    50%
--------------------------------------------------------------
TOTAL                                        88     40    55%

3 passed in 4.32 seconds

This seems trivial now because in this particular moment we’re just testing that the view is returning the data that we put into it in the first place. That’s OK, even trivial tests are still evidence that your code works. You will of course write more unit tests than just this one, though for the moment even those will be small. Tomorrow when we talk about data models and hook those up to our views, testing our views will involve several more bits.

Let’s add a little bit more. This coverage report is not really helpful, as it’s showing coverage for a bunch of files that we either didn’t write ourselves, or haven’t yet written code for. We can alter what files are reported as covered by altering what’s in the .coveragerc file that sits at our project root.

Currently, it looks like this:

[run]
source = expense_tracker
omit =
    expense_tracker/test*

.coveragerc is omitting the “tests.py” file from coverage. In fact, it’s omitting any file or directory whose name starts with “test”. Let’s add other scripts and directories that we don’t need covered to our .coveragerc so we get a better feeling of what actually needs to be covered in our app.

[run]
source = expense_tracker
omit =
    expense_tracker/test*
    expense_tracker/models/*
    expense_tracker/scripts/*
    expense_tracker/data/*
    expense_tracker/views/notfound.py
    expense_tracker/views/__init__.py
    expense_tracker/routes.py
    expense_tracker/__init__.py

[report]
show_missing = True

I’ve omitted the routes.py, __init__.py, and views/__init__.py files because even though we’ve written code for those files, we are not yet at the point of testing how the app is wired together. As such, checking coverage for those files isn’t going to help.

I added in that bottom line so that when the coverage report prints out, the individual missing lines show up.

Now when we run tox we’ll get a more representative coverage report.

---------- coverage: platform darwin, python 3.6.1-final-0 -----------
Name                               Stmts   Miss  Cover   Missing
----------------------------------------------------------------
expense_tracker/views/default.py       7      0   100%

This seems cut down beyond all actual utility, but it’s far more representative of the work we’ve done thus far. We can use this setup to continue building out our site as we please. Tomorrow, we’ll write a different type of test that will show that our app is all wired together in just the right ways, and get more of that delicious coverage.

Recap

Today we got Pyramid working and set up to run a simple “Hello World” app. We went from there to using the cookiecutter shell command to set up a more complex skeleton using a scaffold, complete with the files we’d need to start work toward a larger project. We learned how to connect incoming requests to routes using configuration. We learned how to write view callables to take in a request and return a response. We also learned how to use configuration to connect those view callables to routes.

First we used views to simply write a message onto a browser page. We soon saw that we could also use views to display the contents of an external file, and even display HTML within that file.

Finally, we learned about how to write some basic tests for our app. This way, we know exactly how our app is working in the moment. We also start building a more solid testing foundation for our codebase, so that tomorrow we can write an even greater testing framework.

Tonight you will use views to display your own HTML, complete with whatever CSS styles your project. Tomorrow, we’ll learn about a better way to use Pyramid to serve up HTML via templates, and we’ll begin to write robust tests for our Pyramid app.