diff options
| author | Christoph Zwerschke <cito@online.de> | 2016-04-19 20:07:12 +0200 |
|---|---|---|
| committer | Christoph Zwerschke <cito@online.de> | 2016-04-19 20:07:12 +0200 |
| commit | 3629c49e46207ff5162a82883c14937e6ef4c186 (patch) | |
| tree | 1306181202cb8313f16080789f5b9ab1eeb61d53 /docs | |
| parent | 804ba0b2f434781e77d2b5191f1cd76a490f6610 (diff) | |
| parent | 6c16fb020027fac47e4d2e335cd9e264dba8aa3b (diff) | |
| download | pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.gz pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.tar.bz2 pyramid-3629c49e46207ff5162a82883c14937e6ef4c186.zip | |
Merge remote-tracking branch 'refs/remotes/Pylons/master'
Diffstat (limited to 'docs')
749 files changed, 51442 insertions, 15515 deletions
diff --git a/docs/.gitignore b/docs/.gitignore index 1e9e0413c..30d731d4a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,3 @@ _build -_themes + diff --git a/docs/Makefile b/docs/Makefile index 1d032cf45..546deb30a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,14 +2,15 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = +BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html web pickle htmlhelp latex changes linkcheck @@ -23,66 +24,69 @@ help: @echo " linkcheck to check all external links for integrity" clean: - -rm -rf _build/* + -rm -rf $(BUILDDIR)/* -html: _themes - mkdir -p _build/html _build/doctrees - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html +html: + mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo - @echo "Build finished. The HTML pages are in _build/html." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." text: - mkdir -p _build/text _build/doctrees - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) _build/text + mkdir -p $(BUILDDIR)/text $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo - @echo "Build finished. The HTML pages are in _build/text." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/text." pickle: - mkdir -p _build/pickle _build/doctrees - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle + mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files or run" - @echo " sphinx-web _build/pickle" + @echo " sphinx-web $(BUILDDIR)/pickle" @echo "to start the sphinx-web server." web: pickle -htmlhelp: _themes - mkdir -p _build/htmlhelp _build/doctrees - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp +htmlhelp: + mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in _build/htmlhelp." + ".hhp project file in $(BUILDDIR)/htmlhelp." latex: - mkdir -p _build/latex _build/doctrees - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex - cp _static/*.png _build/latex + mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + cp _static/*.png $(BUILDDIR)/latex ./convert_images.sh - cp _static/latex-warning.png _build/latex - cp _static/latex-note.png _build/latex + cp _static/latex-warning.png $(BUILDDIR)/latex + cp _static/latex-note.png $(BUILDDIR)/latex @echo - @echo "Build finished; the LaTeX files are in _build/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make latexpdf' to build a PDF file from them." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF file is in $(BUILDDIR)/latex." changes: - mkdir -p _build/changes _build/doctrees - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes + mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo - @echo "The overview file is in _build/changes." + @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - mkdir -p _build/linkcheck _build/doctrees - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck + mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ - "or in _build/linkcheck/output.txt." + "or in $(BUILDDIR)/linkcheck/output.txt." epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) _build/epub + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo - @echo "Build finished. The epub file is in _build/epub." + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." -_themes: - git clone git://github.com/Pylons/pylons_sphinx_theme.git _themes diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle new file mode 100644 index 000000000..56e4e13f2 --- /dev/null +++ b/docs/_static/pyramid_request_processing.graffle @@ -0,0 +1,9979 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ActiveLayerIndex</key> + <integer>0</integer> + <key>ApplicationVersion</key> + <array> + <string>com.omnigroup.OmniGrafflePro</string> + <string>139.18.0.187838</string> + </array> + <key>AutoAdjust</key> + <true/> + <key>BackgroundGraphic</key> + <dict> + <key>Bounds</key> + <string>{{0, 0}, {576, 733}}</string> + <key>Class</key> + <string>SolidGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>13</real> + </dict> + <key>ID</key> + <integer>2</integer> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + </dict> + <key>BaseZoom</key> + <integer>0</integer> + <key>CanvasOrigin</key> + <string>{0, 0}</string> + <key>ColumnAlign</key> + <integer>1</integer> + <key>ColumnSpacing</key> + <real>36</real> + <key>CreationDate</key> + <string>2014-11-18 08:33:33 +0000</string> + <key>Creator</key> + <string>Steve Piercy</string> + <key>DisplayScale</key> + <string>1 0/72 in = 1.0000 in</string> + <key>GraphDocumentVersion</key> + <integer>8</integer> + <key>GraphicsList</key> + <array> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 294.65604172230951}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169515</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 CSRF checks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169513</integer> + </dict> + <key>ID</key> + <integer>169514</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.9999760464211, 209.11365574251681}</string> + <string>{239.8333613077798, 209.14732074737549}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169373</integer> + <key>Position</key> + <real>0.47711458802223206</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239.83336130777977, 197.875}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169513</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeTraversal}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169389</integer> + </dict> + <key>ID</key> + <integer>169504</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{344.41668319702148, 411.88506673894034}</string> + <string>{375.5, 411.77232108797347}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169509</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169382</integer> + </dict> + <key>ID</key> + <integer>169433</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 459.27667544230695}</string> + <string>{238.5002713470962, 456.52468399152298}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169370</integer> + <key>Position</key> + <real>0.28820157051086426</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169383</integer> + </dict> + <key>ID</key> + <integer>169432</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 482.12574895537085}</string> + <string>{238.52297468463752, 508.35839132916635}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169370</integer> + <key>Position</key> + <real>0.5668826699256897</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 275.99999999999994}, {105.75002924601222, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169506</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 421.15071036499205}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169507</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, 0.5}</string> + <string>{-0.49999999999999911, 0.49999999999999289}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 312.65604172230951}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169508</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 402.55704269887212}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169509</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 383.90099016834085}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169510</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 350.36561209044055}, {105.66668701171875, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169511</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.74999618530273, 331.26348241170439}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>11</real> + </dict> + <key>ID</key> + <integer>169512</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169422</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169423</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 470.25295298442387}</string> + <string>{238.33861159880226, 482.4262543949045}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169370</integer> + <key>Position</key> + <real>0.42701038718223572</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 471.22620192028251}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169422</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169420</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169421</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.99998733539806, 128.68025330008533}</string> + <string>{239.83340199788393, 128.59152244387357}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169386</integer> + <key>Position</key> + <real>0.35945424437522888</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239.83340199788395, 117.31920169649808}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169420</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{102.1666056315114, 148.28868579864499}, {105.66669464111328, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169418</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.1666056315114, 181.37798023223874}, {105.66669464111328, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169419</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169418</integer> + <integer>169419</integer> + <array/> + </array> + <key>ID</key> + <integer>169417</integer> + <key>Layer</key> + <integer>0</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{102.16666158040482, 272}, {105.66666412353516, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169412</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16666158040482, 305.08929443359375}, {105.66666412353516, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169413</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169412</integer> + <integer>169413</integer> + <array/> + </array> + <key>ID</key> + <integer>169411</integer> + <key>Layer</key> + <integer>0</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169407</integer> + <key>Info</key> + <integer>7</integer> + </dict> + <key>ID</key> + <integer>169410</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{238.74999618530282, 439.80675844512831}</string> + <string>{207.66666666666765, 385.656005859375}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169507</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169407</integer> + <key>Info</key> + <integer>8</integer> + </dict> + <key>ID</key> + <integer>169409</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{239.25039065750093, 276.57837549845181}</string> + <string>{207.66666666666777, 353.07514659563753}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169506</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169381</integer> + </dict> + <key>ID</key> + <integer>169408</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 386.66442959065108}</string> + <string>{155.00000254313238, 422.21209462483216}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169407</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 353.07514659563753}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169407</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{-0.49211360058019871, -0.49251945318722434}</string> + <string>{-0.49211360058019871, 0.49470854679786669}</string> + <string>{0.4984227008620481, 0.48463479169597612}</string> + <string>{0.49842270086204898, -0.5}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169380</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169399</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.9999936421724, 258.44082431579938}</string> + <string>{238.8333613077798, 258.45536063967575}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169372</integer> + <key>Position</key> + <real>0.51973581314086914</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>Group</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{383.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169393</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{383.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169394</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{383.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169395</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view deriver}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{383.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169396</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{383.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169397</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{370.9999504089372, 42.910746256511771}, {132.66667175292969, 184.08924865722656}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169398</integer> + <key>Magnets</key> + <array> + <string>{1, 0.5}</string> + <string>{1, -0.5}</string> + <string>{-1, 0.5}</string> + <string>{-1, -0.5}</string> + <string>{0.5, 1}</string> + <string>{-0.5, 1}</string> + <string>{0.5, -1}</string> + <string>{-0.5, -1}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>CornerRadius</key> + <real>5</real> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>169391</integer> + <key>Layer</key> + <integer>0</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169390</integer> + <key>Layer</key> + <integer>0</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Pad</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Bounds</key> + <string>{{375.5, 400.5}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169389</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169418</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169386</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000170434049, 119.22767858295661}</string> + <string>{154.99995295206804, 148.28868579864499}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169378</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169378</integer> + </dict> + <key>ID</key> + <integer>169385</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 67.727678571434836}</string> + <string>{155.00000254313238, 96.18303707668386}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169377</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 509.6179466247504}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169384</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 497.23589324949899}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169383</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 445.23589324949717}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169382</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 422.21209462483216}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169381</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 247.18303989230026}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169380</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 222.18303707668389}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169379</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 96.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169378</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{102.16667048136482, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169377</integer> + <key>Layer</key> + <integer>0</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress }</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169379</integer> + </dict> + <key>ID</key> + <integer>169373</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.99995295206804, 198.62202930450437}</string> + <string>{155.00000254313238, 222.18303707668389}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169419</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169412</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169372</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.9999936421724, 245.22767856643924}</string> + <string>{154.9999936421724, 272}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169379</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169407</integer> + </dict> + <key>ID</key> + <integer>169371</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{154.9999936421724, 322.33334350585938}</string> + <string>{155.00000254313238, 353.07514659563753}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169413</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9839935302734375}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169384</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169370</integer> + <key>Layer</key> + <integer>0</integer> + <key>Points</key> + <array> + <string>{155.00000254313238, 444.75673611958314}</string> + <string>{155.00000254313238, 509.6179466247504}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169381</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169444</integer> + <key>Info</key> + <integer>6</integer> + </dict> + <key>ID</key> + <integer>169503</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{272.4166717529298, 537.32234122436705}</string> + <string>{420.4999504089364, 515.08928491955714}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169494</integer> + <key>Info</key> + <integer>5</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169444</integer> + </dict> + <key>ID</key> + <integer>169502</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{272.50004831949906, 391.51558277923863}</string> + <string>{420.4999504089364, 472.78869058972316}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169493</integer> + <key>Info</key> + <integer>5</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169450</integer> + </dict> + <key>ID</key> + <integer>169501</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 592.81693102013151}</string> + <string>{239, 583.78422005970799}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169438</integer> + <key>Position</key> + <real>0.28820157051086426</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169451</integer> + </dict> + <key>ID</key> + <integer>169500</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 629.80996162681686}</string> + <string>{239, 640.78422005970981}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169438</integer> + <key>Position</key> + <real>0.5668826699256897</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>Group</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169493</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169494</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, 0.5}</string> + <string>{-0.49999999999999911, 0.49999999999999289}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169495</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169496</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169497</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169498</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169499</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>ID</key> + <integer>169492</integer> + <key>Layer</key> + <integer>1</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169490</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169491</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.166643778483959, 611.77452873049333}</string> + <string>{238.8333613077798, 611.77452873049333}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169490</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169488</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169489</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{82.999986314263907, 140.3328574622312}</string> + <string>{239.83340199788393, 141.59152244387357}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169454</integer> + <key>Position</key> + <real>0.35945424437522888</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169488</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169486</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169487</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169486</integer> + <integer>169487</integer> + <array/> + </array> + <key>ID</key> + <integer>169485</integer> + <key>Layer</key> + <integer>1</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169483</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169484</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169483</integer> + <integer>169484</integer> + <array/> + </array> + <key>ID</key> + <integer>169482</integer> + <key>Layer</key> + <integer>1</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169480</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169481</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169480</integer> + <integer>169481</integer> + <array/> + </array> + <key>ID</key> + <integer>169479</integer> + <key>Layer</key> + <integer>1</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169475</integer> + <key>Info</key> + <integer>7</integer> + </dict> + <key>ID</key> + <integer>169478</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{166.75000762939462, 537.32234122436694}</string> + <string>{135.66666666666765, 485}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169494</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169475</integer> + <key>Info</key> + <integer>8</integer> + </dict> + <key>ID</key> + <integer>169477</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{167.33336141608385, 392.09395827769049}</string> + <string>{135.66666666666777, 452.41914073626253}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169493</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169449</integer> + </dict> + <key>ID</key> + <integer>169476</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 485.50842372576449}</string> + <string>{83.000002543132396, 548.10604731241608}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169475</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169475</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{-0.49211360058019871, -0.49251945318722434}</string> + <string>{-0.49211360058019871, 0.49470854679786669}</string> + <string>{0.4984227008620481, 0.48463479169597612}</string> + <string>{0.49842270086204898, -0.5}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{51.333333333333314, 0}</string> + <string>{-0.66666666666662877, 58.666666666666686}</string> + <string>{0.66673293066804717, -58.666850540458825}</string> + <string>{-16.306719354194399, 0.26652623861849634}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169443</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169474</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{369.66666666666669, 541}</string> + <string>{404.00000000000023, 362}</string> + <string>{420.36749776329049, 302.42112495959606}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.75663</string> + <key>g</key> + <string>0.756618</string> + <key>r</key> + <string>0.75664</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{69.833333333332462, -0.72767857143483639}</string> + <string>{-0.66690523835279691, -51.044605218028948}</string> + <string>{0.66666666666674246, 51.044637362162291}</string> + <string>{-24.333271383961971, -0.13425428344572765}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169443</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169473</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{310.66666666666754, 118.72767857143484}</string> + <string>{399.33333333333417, 216.62202930450439}</string> + <string>{420.37955338188368, 301.45369961823752}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.75663</string> + <key>g</key> + <string>0.756618</string> + <key>r</key> + <string>0.75664</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{-3.9999491373696401, 78.910715080442856}</string> + <string>{92.666683130060392, 0.22547126950667007}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169490</integer> + <key>Info</key> + <integer>3</integer> + </dict> + <key>ID</key> + <integer>169472</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{473.33328247070392, 515.08928491955714}</string> + <string>{344.50002543131501, 611.77452873049333}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169444</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-32.166667938232536, 10.244050343831077}</string> + </array> + <key>ID</key> + <integer>169471</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{344.96346869509995, 346.26317428240412}</string> + <string>{389.8333346048999, 328.08928298950207}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169456</integer> + <key>Info</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-28.500001271565793, 8.3333333333333144}</string> + </array> + <key>ID</key> + <integer>169470</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{344.98861594084059, 323.71068461220347}</string> + <string>{391.1666679382335, 313.6666666666664}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169455</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-40, 1.5446373167492311}</string> + </array> + <key>ID</key> + <integer>169469</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{344.9995533451783, 301.1612218744512}</string> + <string>{394.50000127156665, 299.00000000000045}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169442</integer> + <key>Info</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{8.5833282470703125, -10.244596987647753}</string> + <string>{0, 0}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169457</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169468</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{272.41667175292969, 509.40064951817902}</string> + <string>{285.5, 503.81699882234847}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169496</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169448</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169467</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 300.27229985501288}</string> + <string>{238.34892458824362, 260.57913893040109}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169440</integer> + <key>Position</key> + <real>0.51973581314086914</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>Group</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169460</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169461</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169462</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169463</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169464</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169465</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169466</integer> + <key>Magnets</key> + <array> + <string>{1, 0.5}</string> + <string>{1, -0.5}</string> + <string>{-1, 0.5}</string> + <string>{-1, -0.5}</string> + <string>{0.5, 1}</string> + <string>{-0.5, 1}</string> + <string>{0.5, -1}</string> + <string>{-0.5, -1}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>CornerRadius</key> + <real>5</real> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>169459</integer> + <key>Layer</key> + <integer>1</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169458</integer> + <key>Layer</key> + <integer>1</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Pad</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Bounds</key> + <string>{{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169457</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169456</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPForbidden}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169455</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 PredicateMismatch}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169486</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169454</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000001850648417, 128.2276785555359}</string> + <string>{82.999949137370791, 166.28868579864502}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169446</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169446</integer> + </dict> + <key>ID</key> + <integer>169453</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 67.727678571434836}</string> + <string>{83.000002543132396, 105.18303707668386}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169445</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169452</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169451</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169450</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169449</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169448</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169447</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169446</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169445</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress }</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169444</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{-0.49999999999999956, -0.5}</string> + <string>{-0.49999999999999956, 0.5}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 notfound_view / forbidden_view / exception_view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169443</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169442</integer> + <key>Layer</key> + <integer>1</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPNotFound}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169447</integer> + </dict> + <key>ID</key> + <integer>169441</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{82.999949137370791, 216.62202930450437}</string> + <string>{83.000002543132396, 240.18303707668389}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169487</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169480</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169440</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 263.22767855635425}</string> + <string>{82.999997456869679, 335}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169447</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169475</integer> + </dict> + <key>ID</key> + <integer>169439</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 385.33334350585938}</string> + <string>{83.000002543132396, 452.41914073626253}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169481</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9839935302734375}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169452</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169438</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 571.15068879140836}</string> + <string>{83.000002543132396, 671.51189931233421}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169449</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.055999755859375}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169483</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169437</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{473.33328247070392, 313.2113088426139}</string> + <string>{473.33338419596407, 338.15028762817326}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169443</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840011596679688}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169444</integer> + </dict> + <key>ID</key> + <integer>169436</integer> + <key>Layer</key> + <integer>1</integer> + <key>Points</key> + <array> + <string>{473.33338359264764, 388.98363112285512}</string> + <string>{473.33328247070392, 472.78869058972316}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169484</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169358</integer> + <key>Info</key> + <integer>6</integer> + </dict> + <key>ID</key> + <integer>169359</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{272.4166717529298, 537.32234122436705}</string> + <string>{420.4999504089364, 515.08928491955714}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169206</integer> + <key>Info</key> + <integer>5</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169358</integer> + </dict> + <key>ID</key> + <integer>169360</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{272.50004831949906, 391.51558277923863}</string> + <string>{420.4999504089364, 472.78869058972316}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169205</integer> + <key>Info</key> + <integer>5</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169044</integer> + </dict> + <key>ID</key> + <integer>169130</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 592.81693102013151}</string> + <string>{239, 583.78422005970799}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169128</integer> + <key>Position</key> + <real>0.28820157051086426</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169045</integer> + </dict> + <key>ID</key> + <integer>169129</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 629.80996162681686}</string> + <string>{239, 640.78422005970981}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169128</integer> + <key>Position</key> + <real>0.5668826699256897</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>Group</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{166.8333613077798, 391.51558277923863}, {105.66668701171875, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169205</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 authorization}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 518.66629314423074}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169206</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, 0.5}</string> + <string>{-0.49999999999999911, 0.49999999999999289}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 410.17162450154819}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169207</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{0.50000000000000089, -0.49999999999999645}</string> + <string>{-0.49526813868737474, -0.4689979626999552}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 decorators ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 500.07262547811081}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169208</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response adapter}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 481.41657294757954}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169209</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 447.88119486967923}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169210</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{166.75000762939453, 428.77906519094307}, {105.66666412353516, 18.656048080136394}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169211</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.637876</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view mapper ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>ID</key> + <integer>169204</integer> + <key>Layer</key> + <integer>2</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169085</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169086</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.166643778483959, 611.77452873049333}</string> + <string>{238.8333613077798, 611.77452873049333}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 600.50220798311784}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169085</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewResponse}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169083</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169084</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{82.999986314263907, 140.3328574622312}</string> + <string>{239.83340199788393, 141.59152244387357}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169048</integer> + <key>Position</key> + <real>0.35945424437522888</real> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239.83340199788395, 130.31920169649808}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169083</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 NewRequest}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{30.166605631511416, 166.28868579864499}, {105.66668701171875, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169081</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 URL dispatch}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166605631511416, 199.37798023223874}, {105.66668701171875, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169082</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 route predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169081</integer> + <integer>169082</integer> + <array/> + </array> + <key>ID</key> + <integer>169080</integer> + <key>Layer</key> + <integer>2</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{420.5000406901047, 338.15028762817326}, {105.66668701171875, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169355</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.5000406901047, 371.23958206176701}, {105.66668701171875, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169356</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169355</integer> + <integer>169356</integer> + <array/> + </array> + <key>ID</key> + <integer>169354</integer> + <key>Layer</key> + <integer>2</integer> + </dict> + <dict> + <key>Class</key> + <string>TableGroup</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{30.166661580404835, 335}, {105.66667175292969, 33.08929443359375}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169075</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view lookup}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166661580404835, 368.08929443359375}, {105.66667175292969, 17.244049072265625}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169076</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 predicates}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + </array> + <key>GridH</key> + <array> + <integer>169075</integer> + <integer>169076</integer> + <array/> + </array> + <key>ID</key> + <integer>169074</integer> + <key>Layer</key> + <integer>2</integer> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169070</integer> + <key>Info</key> + <integer>7</integer> + </dict> + <key>ID</key> + <integer>169073</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{166.75000762939462, 537.32234122436694}</string> + <string>{135.66666666666765, 485}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169206</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169070</integer> + <key>Info</key> + <integer>8</integer> + </dict> + <key>ID</key> + <integer>169072</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{167.33336141608385, 392.09395827769049}</string> + <string>{135.66666666666777, 452.41914073626253}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.755269</string> + <key>g</key> + <string>0.755239</string> + <key>r</key> + <string>0.75529</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>11</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169205</integer> + <key>Info</key> + <integer>6</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169043</integer> + </dict> + <key>ID</key> + <integer>169071</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 485.50842372576449}</string> + <string>{83.000002543132396, 548.10604731241608}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169070</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 452.41914073626253}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169070</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{-0.49211360058019871, -0.49251945318722434}</string> + <string>{-0.49211360058019871, 0.49470854679786669}</string> + <string>{0.4984227008620481, 0.48463479169597612}</string> + <string>{0.49842270086204898, -0.5}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view pipeline}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{51.333333333333314, 0}</string> + <string>{-0.66666666666662877, 58.666666666666686}</string> + <string>{0.66673293066804717, -58.666850540458825}</string> + <string>{-16.306719354194399, 0.26652623861849634}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169344</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169345</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{369.66666666666669, 541}</string> + <string>{404.00000000000023, 362}</string> + <string>{420.36749776329049, 302.42112495959606}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.75663</string> + <key>g</key> + <string>0.756618</string> + <key>r</key> + <string>0.75664</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{69.833333333332462, -0.72767857143483639}</string> + <string>{-0.66690523835279691, -51.044605218028948}</string> + <string>{0.66666666666674246, 51.044637362162291}</string> + <string>{-24.333271383961971, -0.13425428344572765}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169344</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169346</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{310.66666666666754, 118.72767857143484}</string> + <string>{399.33333333333417, 216.62202930450439}</string> + <string>{420.37955338188368, 301.45369961823752}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.75663</string> + <key>g</key> + <string>0.756618</string> + <key>r</key> + <string>0.75664</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{-3.9999491373696401, 78.910715080442856}</string> + <string>{92.666683130060392, 0.22547126950667007}</string> + </array> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169085</integer> + <key>Info</key> + <integer>3</integer> + </dict> + <key>ID</key> + <integer>169361</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{473.33328247070392, 515.08928491955714}</string> + <string>{344.50002543131501, 611.77452873049333}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169358</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-32.166667938232536, 10.244050343831077}</string> + </array> + <key>ID</key> + <integer>169341</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{344.96346869509995, 346.26317428240412}</string> + <string>{389.8333346048999, 328.08928298950207}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169340</integer> + <key>Info</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-28.500001271565793, 8.3333333333333144}</string> + </array> + <key>ID</key> + <integer>169337</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{344.98861594084059, 323.71068461220347}</string> + <string>{391.1666679382335, 313.6666666666664}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169336</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{31.999987284342428, -14.081351280212308}</string> + <string>{-40, 1.5446373167492311}</string> + </array> + <key>ID</key> + <integer>169333</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{344.9995533451783, 301.1612218744512}</string> + <string>{394.50000127156665, 299.00000000000045}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169332</integer> + <key>Info</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{8.5833282470703125, -10.244596987647753}</string> + <string>{0, 0}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169051</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169062</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{272.41667175292969, 509.40064951817902}</string> + <string>{285.5, 503.81699882234847}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>Pattern</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169208</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169042</integer> + <key>Info</key> + <integer>4</integer> + </dict> + <key>ID</key> + <integer>169061</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 300.27229985501288}</string> + <string>{238.34892458824362, 260.57913893040109}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>0</string> + <key>Legacy</key> + <true/> + <key>Pattern</key> + <integer>2</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169034</integer> + <key>Position</key> + <real>0.51973581314086914</real> + </dict> + </dict> + <dict> + <key>Class</key> + <string>Group</string> + <key>Graphics</key> + <array> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 214.61452811107179}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169054</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 130.51770718892479}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169055</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 internal process}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 91.940789540609359}, {105.66666412353516, 33.089282989501953}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169056</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 external process (middleware, tween)}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 158.54998334248924}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169057</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 186.58225949605369}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169058</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 callback}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{419.66662216186666, 63.908513387045019}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169059</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 event}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{406.9999504089372, 42.910746256511771}, {132.66667175292969, 207.81692504882812}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169060</integer> + <key>Magnets</key> + <array> + <string>{1, 0.5}</string> + <string>{1, -0.5}</string> + <string>{-1, 0.5}</string> + <string>{-1, -0.5}</string> + <string>{0.5, 1}</string> + <string>{-0.5, 1}</string> + <string>{0.5, -1}</string> + <string>{-0.5, -1}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>CornerRadius</key> + <real>5</real> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Align</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\b\fs20 \cf0 Legend}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>TextPlacement</key> + <integer>0</integer> + </dict> + </array> + <key>ID</key> + <integer>169053</integer> + <key>Layer</key> + <integer>2</integer> + </dict> + <dict> + <key>Bounds</key> + <string>{{233.5000012715667, 20.000000000000934}, {116, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169052</integer> + <key>Layer</key> + <integer>2</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Pad</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Bounds</key> + <string>{{285.5, 492.544677734375}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169051</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 BeforeRender}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83337529500417, 335.17855853126167}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169340</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPForbidden}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83337529500417, 312.54463200342093}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169336</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 PredicateMismatch}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169081</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169048</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000001850648417, 128.2276785555359}</string> + <string>{82.999949137370791, 166.28868579864502}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169040</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169040</integer> + </dict> + <key>ID</key> + <integer>169047</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 67.727678571434836}</string> + <string>{83.000002543132396, 105.18303707668386}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169039</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 671.51189931233432}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169046</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 629.51189931233432}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169045</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 finished callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{239, 572.5118993123325}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169044</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.327428</string> + <key>g</key> + <string>0.81823</string> + <key>r</key> + <string>0.995566</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 response callbacks}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 548.10604731241608}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169043</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween egress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336130777977, 249.18303989230026}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169042</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 ContextFound}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 240.18303707668389}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169041</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 traversal}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 105.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169040</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 tween ingress}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{30.166670481364818, 45.18303707668386}, {105.66666412353516, 22.544641494750977}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169039</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999208</string> + <key>g</key> + <string>0.811343</string> + <key>r</key> + <string>0.644457</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 middleware ingress }</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.49995040893634, 472.78869058972316}, {105.66666412353516, 42.300594329833984}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169358</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + <string>{-0.49999999999999956, -0.5}</string> + <string>{-0.49999999999999956, 0.5}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 notfound_view / forbidden_view / exception_view}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{420.49995040893634, 290.66666666666691}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169344</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 exception}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{238.83336512247806, 289.91071033477789}, {105.66666412353516, 22.544642175946908}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169332</integer> + <key>Layer</key> + <integer>2</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + <key>ShadowVector</key> + <string>{2, 2}</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 HTTPNotFound}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169041</integer> + </dict> + <key>ID</key> + <integer>169035</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{82.999949137370791, 216.62202930450437}</string> + <string>{83.000002543132396, 240.18303707668389}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169082</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.05596923828125}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169075</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169034</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 263.22767855635425}</string> + <string>{82.999997456869679, 335}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169041</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -8.9999999999999432}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169070</integer> + </dict> + <key>ID</key> + <integer>169033</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{82.999997456869679, 385.33334350585938}</string> + <string>{83.000002543132396, 452.41914073626253}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169076</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9839935302734375}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169046</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169128</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{83.000002543132396, 571.15068879140836}</string> + <string>{83.000002543132396, 671.51189931233421}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169043</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 7.055999755859375}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169355</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169357</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{473.33328247070392, 313.2113088426139}</string> + <string>{473.33338419596407, 338.15028762817326}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169344</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840011596679688}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169358</integer> + </dict> + <key>ID</key> + <integer>169362</integer> + <key>Layer</key> + <integer>2</integer> + <key>Points</key> + <array> + <string>{473.33338359264764, 388.98363112285512}</string> + <string>{473.33328247070392, 472.78869058972316}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169356</integer> + </dict> + </dict> + </array> + <key>GridInfo</key> + <dict/> + <key>GuidesLocked</key> + <string>NO</string> + <key>GuidesVisible</key> + <string>YES</string> + <key>HPages</key> + <integer>1</integer> + <key>HorizontalGuides</key> + <array> + <real>209.875</real> + </array> + <key>ImageCounter</key> + <integer>3</integer> + <key>KeepToScale</key> + <false/> + <key>Layers</key> + <array> + <dict> + <key>Lock</key> + <string>NO</string> + <key>Name</key> + <string>no exceptions</string> + <key>Print</key> + <string>YES</string> + <key>View</key> + <string>YES</string> + </dict> + <dict> + <key>Lock</key> + <string>NO</string> + <key>Name</key> + <string>exceptions only</string> + <key>Print</key> + <string>YES</string> + <key>View</key> + <string>NO</string> + </dict> + <dict> + <key>Lock</key> + <string>NO</string> + <key>Name</key> + <string>all</string> + <key>Print</key> + <string>YES</string> + <key>View</key> + <string>NO</string> + </dict> + </array> + <key>LayoutInfo</key> + <dict> + <key>Animate</key> + <string>NO</string> + <key>circoMinDist</key> + <real>18</real> + <key>circoSeparation</key> + <real>0.0</real> + <key>layoutEngine</key> + <string>dot</string> + <key>neatoSeparation</key> + <real>0.0</real> + <key>twopiSeparation</key> + <real>0.0</real> + </dict> + <key>LinksVisible</key> + <string>NO</string> + <key>MagnetsVisible</key> + <string>NO</string> + <key>MasterSheets</key> + <array/> + <key>ModificationDate</key> + <string>2016-04-13 08:32:47 +0000</string> + <key>Modifier</key> + <string>Steve Piercy</string> + <key>NotesVisible</key> + <string>NO</string> + <key>Orientation</key> + <integer>2</integer> + <key>OriginVisible</key> + <string>NO</string> + <key>PageBreaks</key> + <string>YES</string> + <key>PrintInfo</key> + <dict> + <key>NSBottomMargin</key> + <array> + <string>float</string> + <string>41</string> + </array> + <key>NSHorizonalPagination</key> + <array> + <string>coded</string> + <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string> + </array> + <key>NSLeftMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSPaperSize</key> + <array> + <string>size</string> + <string>{612, 792}</string> + </array> + <key>NSPrintReverseOrientation</key> + <array> + <string>int</string> + <string>0</string> + </array> + <key>NSRightMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSTopMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + </dict> + <key>PrintOnePage</key> + <false/> + <key>ReadOnly</key> + <string>NO</string> + <key>RowAlign</key> + <integer>1</integer> + <key>RowSpacing</key> + <real>36</real> + <key>SheetTitle</key> + <string>Request Processing</string> + <key>SmartAlignmentGuidesActive</key> + <string>YES</string> + <key>SmartDistanceGuidesActive</key> + <string>YES</string> + <key>UniqueID</key> + <integer>1</integer> + <key>UseEntirePage</key> + <false/> + <key>VPages</key> + <integer>1</integer> + <key>WindowInfo</key> + <dict> + <key>CurrentSheet</key> + <integer>0</integer> + <key>ExpandedCanvases</key> + <array> + <dict> + <key>name</key> + <string>Request Processing</string> + </dict> + </array> + <key>Frame</key> + <string>{{35, 93}, {2284, 1325}}</string> + <key>ListView</key> + <true/> + <key>OutlineWidth</key> + <integer>178</integer> + <key>RightSidebar</key> + <true/> + <key>ShowRuler</key> + <true/> + <key>Sidebar</key> + <true/> + <key>SidebarWidth</key> + <integer>163</integer> + <key>VisibleRegion</key> + <string>{{110.125, 77.875}, {239.125, 146.375}}</string> + <key>Zoom</key> + <real>8</real> + <key>ZoomValues</key> + <array> + <array> + <string>Request Processing</string> + <real>8</real> + <real>4</real> + </array> + </array> + </dict> +</dict> +</plist> diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png Binary files differnew file mode 100644 index 000000000..2f44f4824 --- /dev/null +++ b/docs/_static/pyramid_request_processing.png diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg new file mode 100644 index 000000000..03f6d56fa --- /dev/null +++ b/docs/_static/pyramid_request_processing.svg @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2016-04-13 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processing</title><rect fill="white" width="576" height="733"/><g><title>no exceptions</title><path d="M 155 444.75674 C 155 450.64061 155 486.2592 155 502.71617" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 322.33334 C 154.99999 327.72413 155 337.74646 155 346.1775" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99999 245.22768 C 154.99999 250.5417 154.99999 257.93189 154.99999 265.10145" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 154.99995 198.62203 C 154.99995 203.74682 154.99998 209.1909 154.99999 215.28222" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="45.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 50.455358)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="4.7596016" y="10" textLength="88.92578">middleware ingress </tspan></text><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="96.183037" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 101.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.983723" y="10" textLength="61.69922">tween ingress</tspan></text><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="102.16667" y="222.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 227.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="28.660969" y="10" textLength="38.344727">traversal</tspan></text><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="247.18304" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 252.45536)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.424641" y="10" textLength="62.817383">ContextFound</tspan></text><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="422.2121" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 427.48442)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.094563" y="10" textLength="59.47754">tween egress</tspan></text><rect x="239" y="445.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="445.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 450.50821)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3113594" y="10" textLength="85.043945">response callbacks</tspan></text><rect x="239" y="497.2359" width="105.666664" height="22.544641" fill="#fed153"/><rect x="239" y="497.2359" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244 502.5082)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.6463203" y="10" textLength="5">fi</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="13.64632" y="10" textLength="73.374023">nished callbacks</tspan></text><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" fill="#a4cfff"/><rect x="102.16667" y="509.61795" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 514.89027)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.8704414" y="10" textLength="83.92578">middleware egress</tspan></text><path d="M 155 67.72768 C 155 73.048893 155 81.55558 155 89.2853" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 155 119.22768 C 155 124.62026 154.99997 133.48763 154.99996 141.38632" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" fill="#dfbeff"/><rect x="375.5" y="400.5" width="105.666664" height="22.544642" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(380.5 405.77232)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.702961" y="10" textLength="62.260742">BeforeRender</tspan></text><text transform="translate(233.5 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".31445312" y="11" textLength="115.371094">Request Processing</tspan></text><path d="M 375.99995 42.910746 L 498.66662 42.910746 C 501.42805 42.910746 503.66662 45.149323 503.66662 47.910746 L 503.66662 222 C 503.66662 224.76142 501.42805 227 498.66662 227 L 375.99995 227 C 373.23853 227 370.99995 224.76142 370.99995 222 L 370.99995 47.910746 C 370.99995 45.149323 373.23853 42.910746 375.99995 42.910746 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(375.99995 42.910746)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="35.55664">Legend</tspan></text><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="383.66662" y="63.908513" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 69.180834)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.601887" y="10" textLength="24.46289">event</tspan></text><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" fill="#fed153"/><rect x="383.66662" y="186.58226" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 191.85458)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.769367" y="10" textLength="36.12793">callback</tspan></text><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" fill="#ffff6c"/><rect x="383.66662" y="158.54998" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 163.8223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.158527" y="10" textLength="53.34961">view deriver</tspan></text><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" fill="#a4cfff"/><rect x="383.66662" y="91.94079" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 96.48543)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.148762" y="10" textLength="76.14746">external process </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="2.8162422" y="22" textLength="90.03418">(middleware, tween)</tspan></text><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" fill="#d2ffd0"/><rect x="383.66662" y="130.51771" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(388.66662 135.79003)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.537922" y="10" textLength="70.59082">internal process</tspan></text><line x1="154.99999" y1="258.44082" x2="238.83336" y2="258.45536" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" fill="#ffff6c"/><rect x="102.16667" y="353.07515" width="105.666664" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16667 363.61979)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.205402" y="10" textLength="57.25586">view pipeline</tspan></text><path d="M 155 386.66443 C 155 392.17252 155 405.5052 155 415.30935" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="239.25039" y1="276.57838" x2="207.66667" y2="353.07515" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><line x1="238.75" y1="439.80676" x2="207.66667" y2="385.656" stroke="#c1c1c1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,3"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" fill="#d2ffd0"/><rect x="102.16666" y="305.0893" width="105.666664" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 307.71132)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="24.764484" y="10" textLength="46.137695">predicates</tspan></text><rect x="102.16666" y="272" width="105.666664" height="33.089294" fill="#d2ffd0"/><rect x="102.16666" y="272" width="105.666664" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.16666 282.54465)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="21.707844" y="10" textLength="52.250977">view lookup</tspan></text><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" fill="#d2ffd0"/><rect x="102.166606" y="181.37798" width="105.666695" height="17.244049" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 184)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.978855" y="10" textLength="71.708984">route predicates</tspan></text><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" fill="#d2ffd0"/><rect x="102.166606" y="148.28869" width="105.666695" height="33.089294" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(107.166606 158.83333)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.001804" y="10" textLength="20.004883">URL</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.640476" y="10" textLength="40.024414"> dispatch</tspan></text><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.8334" y="117.3192" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.8334 122.59152)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.207844" y="10" textLength="57.250977">NewRequest</tspan></text><line x1="154.99999" y1="128.68025" x2="239.8334" y2="128.59152" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="238.83336" y="471.2262" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.83336 476.49852)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.316242" y="10" textLength="65.03418">NewResponse</tspan></text><line x1="155" y1="470.25295" x2="238.33861" y2="482.42625" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="331.26348" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 334.5915)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="1.9812927" y="10" textLength="91.7041">view mapper ingress</tspan></text><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" fill="#ffff6c"/><rect x="238.75" y="350.36561" width="105.66669" height="33.089283" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 360.91025)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="37.830902" y="10" textLength="20.004883">view</tspan></text><rect x="238.75" y="383.901" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="383.901" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 387.22901)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="3.0921326" y="10" textLength="89.48242">view mapper egress</tspan></text><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="402.55704" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 405.88507)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="8.917328" y="10" textLength="77.83203">response adapter</tspan></text><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="312.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 315.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="6.7029724" y="10" textLength="82.26074">decorators ingress</tspan></text><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="421.1507" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 424.47873)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="7.8138123" y="10" textLength="80.039062">decorators egress</tspan></text><rect x="238.75" y="276" width="105.75003" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="276" width="105.75003" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 279.32802)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.244644" y="10" textLength="57.260742">authorization</tspan></text><line x1="155" y1="482.12575" x2="238.52297" y2="508.3584" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="155" y1="459.27668" x2="238.50027" y2="456.52468" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><line x1="344.41668" y1="411.88507" x2="375.5" y2="411.77232" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" fill="#dfbeff"/><rect x="239.83336" y="197.875" width="105.666664" height="22.544641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(244.83336 203.14732)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.44759" y="10" textLength="35.57129">BeforeT</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="47.652668" y="10" textLength="35.566406">raversal</tspan></text><line x1="154.99998" y1="209.11366" x2="239.83336" y2="209.14732" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="1,4"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" fill="#ffffa3"/><rect x="238.75" y="294.65604" width="105.66669" height="18.656048" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(243.75 297.98407)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="17.27182" y="10" textLength="61.123047">CSRF checks</tspan></text></g></g></svg> diff --git a/docs/_static/pyramid_router.graffle b/docs/_static/pyramid_router.graffle new file mode 100644 index 000000000..217878426 --- /dev/null +++ b/docs/_static/pyramid_router.graffle @@ -0,0 +1,1621 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ActiveLayerIndex</key> + <integer>0</integer> + <key>ApplicationVersion</key> + <array> + <string>com.omnigroup.OmniGrafflePro</string> + <string>139.18.0.187838</string> + </array> + <key>AutoAdjust</key> + <true/> + <key>BackgroundGraphic</key> + <dict> + <key>Bounds</key> + <string>{{0, 0}, {576, 733}}</string> + <key>Class</key> + <string>SolidGraphic</string> + <key>ID</key> + <integer>2</integer> + <key>Style</key> + <dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + </dict> + <key>BaseZoom</key> + <integer>0</integer> + <key>CanvasOrigin</key> + <string>{0, 0}</string> + <key>ColumnAlign</key> + <integer>1</integer> + <key>ColumnSpacing</key> + <real>36</real> + <key>CreationDate</key> + <string>2014-12-01 08:25:13 +0000</string> + <key>Creator</key> + <string>Steve Piercy</string> + <key>DisplayScale</key> + <string>1 0/72 in = 1 0/72 in</string> + <key>GraphDocumentVersion</key> + <integer>8</integer> + <key>GraphicsList</key> + <array> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169413</integer> + </dict> + <key>ID</key> + <integer>169414</integer> + <key>Points</key> + <array> + <string>{202.04165903727232, 501.05557886759294}</string> + <string>{202.04165903727232, 528.77776209513161}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169412</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666686, 528.77776209513161}, {195.24998474121094, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169413</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.999449</string> + <key>g</key> + <string>0.743511</string> + <key>r</key> + <string>0.872276</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b response}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666657, 471.55557886759294}, {195.24998474121094, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169412</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Invoke the +\b view callable +\b0 ,\ +which returns a +\b response}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{291.21562524160186, 379.55555343627816}, {26, 24}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169411</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>169410</integer> + <key>Offset</key> + <real>7.3333320617675781</real> + <key>Position</key> + <real>0.4865129292011261</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 No}</string> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{34.791667904111534, 0}</string> + <string>{-33.999994913736998, 0}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169409</integer> + </dict> + <key>ID</key> + <integer>169410</integer> + <key>Points</key> + <array> + <string>{280.85416589389337, 398.88888549804574}</string> + <string>{327.47912214508739, 398.88888549804574}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169404</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{327.47912214508739, 384.38888549804574}, {156.62496948242188, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169409</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b Forbidden View}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{175.11595161998204, 438.9999954213917}, {30, 24}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169408</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Yes}</string> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169412</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169407</integer> + <key>Points</key> + <array> + <string>{202.04165267944353, 437.33333079020139}</string> + <string>{202.04165903727204, 471.55557886759294}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169404</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{171.708317756653, 329.24978243601743}, {30, 24}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169406</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>169405</integer> + <key>Offset</key> + <real>-15.333334922790527</real> + <key>Position</key> + <real>0.45895844697952271</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Yes}</string> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169404</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169405</integer> + <key>Points</key> + <array> + <string>{202.04165267944353, 326.72223360222029}</string> + <string>{202.04165267944353, 360.44446818033811}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{123.72916793823259, 360.44446818033811}, {156.62496948242188, 76.888862609863281}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169404</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Diamond</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Current user has +\b authorization +\b0 to invoke the view callable?}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{283.07625736262997, 281.88889694213805}, {26, 24}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169403</integer> + <key>Line</key> + <dict> + <key>ID</key> + <integer>169402</integer> + <key>Offset</key> + <real>7.3333320617675781</real> + <key>Position</key> + <real>0.4865129292011261</real> + <key>RotationType</key> + <integer>0</integer> + </dict> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 No}</string> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{34.791667904111534, 0}</string> + <string>{-33.999994913736998, 0}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169401</integer> + </dict> + <key>ID</key> + <integer>169402</integer> + <key>Points</key> + <array> + <string>{265.20833208871704, 301.22222900390562}</string> + <string>{327.47911580403627, 301.22222900390562}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>3</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{327.47911580403627, 286.72222900390562}, {156.62496948242188, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169401</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.756045</string> + <key>g</key> + <string>0.75004</string> + <key>r</key> + <string>0.994455</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Return the +\b Not Found View}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>3</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169400</integer> + <key>Points</key> + <array> + <string>{202.04165903727255, 251}</string> + <string>{202.04165776570633, 276.22223154703772}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169393</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{139.37498982747391, 276.22223154703778}, {125.33333587646484, 50}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>3</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Diamond</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.422927</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>1</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 View callable found?}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169393</integer> + <key>Info</key> + <integer>2</integer> + </dict> + <key>ID</key> + <integer>169396</integer> + <key>Points</key> + <array> + <string>{202.04165903727255, 196.77777862548834}</string> + <string>{202.04165903727255, 222}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169392</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169392</integer> + </dict> + <key>ID</key> + <integer>169395</integer> + <key>Points</key> + <array> + <string>{202.04165903727255, 142.55555725097662}</string> + <string>{202.04165903727255, 167.77777862548834}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>169391</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Class</key> + <string>LineGraphic</string> + <key>ControlPoints</key> + <array> + <string>{0, 6.9840087890625}</string> + <string>{0, -9}</string> + </array> + <key>FontInfo</key> + <dict> + <key>Color</key> + <dict> + <key>w</key> + <string>0</string> + </dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>Head</key> + <dict> + <key>ID</key> + <integer>169391</integer> + </dict> + <key>ID</key> + <integer>169385</integer> + <key>Points</key> + <array> + <string>{202.04165903727255, 82.666667938232479}</string> + <string>{202.04165903727255, 107.88888931274418}</string> + </array> + <key>Style</key> + <dict> + <key>stroke</key> + <dict> + <key>Bezier</key> + <true/> + <key>Color</key> + <dict> + <key>b</key> + <string>0.0980392</string> + <key>g</key> + <string>0.0980392</string> + <key>r</key> + <string>0.0980392</string> + </dict> + <key>HeadArrow</key> + <string>SharpArrow</string> + <key>Legacy</key> + <true/> + <key>LineType</key> + <integer>1</integer> + <key>TailArrow</key> + <string>0</string> + </dict> + </dict> + <key>Tail</key> + <dict> + <key>ID</key> + <integer>19</integer> + <key>Info</key> + <integer>1</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666708, 222}, {195.24998474121094, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169393</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Look up a +\b view callable +\b0 in the +\b registry +\b0 using the +\b context +\b0 and +\b view name}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666708, 167.77777862548834}, {195.24998474121094, 29}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169392</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs20 \cf0 Traversal +\b0 locates\ +the +\b context +\b0 and +\b view name}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666708, 107.88888931274418}, {195.24998474121094, 34.666667938232422}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>169391</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Traverse the model graph\ +from the +\b root +\b0 using the +\b path}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{104.41666666666708, 48.000000000000043}, {195.24998474121094, 34.666667938232422}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>ID</key> + <integer>19</integer> + <key>Magnets</key> + <array> + <string>{0, 1}</string> + <string>{0, -1}</string> + <string>{1, 0}</string> + <string>{-1, 0}</string> + </array> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Color</key> + <dict> + <key>b</key> + <string>0.815377</string> + <key>g</key> + <string>1</string> + <key>r</key> + <string>0.820561</string> + </dict> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Obtain a root object from the +\b root factory}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + </dict> + <dict> + <key>Bounds</key> + <string>{{229.04165903727255, 20.000000000000934}, {90, 14}}</string> + <key>Class</key> + <string>ShapedGraphic</string> + <key>FitText</key> + <string>YES</string> + <key>Flow</key> + <string>Resize</string> + <key>FontInfo</key> + <dict> + <key>Font</key> + <string>Helvetica</string> + <key>Size</key> + <real>12</real> + </dict> + <key>ID</key> + <integer>169390</integer> + <key>Shape</key> + <string>Rectangle</string> + <key>Style</key> + <dict> + <key>fill</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>shadow</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + <key>stroke</key> + <dict> + <key>Draws</key> + <string>NO</string> + </dict> + </dict> + <key>Text</key> + <dict> + <key>Pad</key> + <integer>0</integer> + <key>Text</key> + <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 <%Canvas%>}</string> + <key>VerticalPad</key> + <integer>0</integer> + </dict> + <key>Wrap</key> + <string>NO</string> + </dict> + </array> + <key>GridInfo</key> + <dict/> + <key>GuidesLocked</key> + <string>NO</string> + <key>GuidesVisible</key> + <string>YES</string> + <key>HPages</key> + <integer>1</integer> + <key>ImageCounter</key> + <integer>1</integer> + <key>KeepToScale</key> + <false/> + <key>Layers</key> + <array> + <dict> + <key>Lock</key> + <string>NO</string> + <key>Name</key> + <string>Layer 1</string> + <key>Print</key> + <string>YES</string> + <key>View</key> + <string>YES</string> + </dict> + </array> + <key>LayoutInfo</key> + <dict> + <key>Animate</key> + <string>NO</string> + <key>circoMinDist</key> + <real>18</real> + <key>circoSeparation</key> + <real>0.0</real> + <key>layoutEngine</key> + <string>dot</string> + <key>neatoSeparation</key> + <real>0.0</real> + <key>twopiSeparation</key> + <real>0.0</real> + </dict> + <key>LinksVisible</key> + <string>NO</string> + <key>MagnetsVisible</key> + <string>NO</string> + <key>MasterSheets</key> + <array/> + <key>ModificationDate</key> + <string>2014-12-01 09:19:51 +0000</string> + <key>Modifier</key> + <string>Steve Piercy</string> + <key>NotesVisible</key> + <string>NO</string> + <key>Orientation</key> + <integer>2</integer> + <key>OriginVisible</key> + <string>NO</string> + <key>PageBreaks</key> + <string>YES</string> + <key>PrintInfo</key> + <dict> + <key>NSBottomMargin</key> + <array> + <string>float</string> + <string>41</string> + </array> + <key>NSHorizonalPagination</key> + <array> + <string>coded</string> + <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string> + </array> + <key>NSLeftMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSPaperSize</key> + <array> + <string>size</string> + <string>{612, 792}</string> + </array> + <key>NSPrintReverseOrientation</key> + <array> + <string>int</string> + <string>0</string> + </array> + <key>NSRightMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + <key>NSTopMargin</key> + <array> + <string>float</string> + <string>18</string> + </array> + </dict> + <key>PrintOnePage</key> + <false/> + <key>ReadOnly</key> + <string>NO</string> + <key>RowAlign</key> + <integer>1</integer> + <key>RowSpacing</key> + <real>36</real> + <key>SheetTitle</key> + <string>Pyramid Router</string> + <key>SmartAlignmentGuidesActive</key> + <string>YES</string> + <key>SmartDistanceGuidesActive</key> + <string>YES</string> + <key>UniqueID</key> + <integer>1</integer> + <key>UseEntirePage</key> + <false/> + <key>VPages</key> + <integer>1</integer> + <key>WindowInfo</key> + <dict> + <key>CurrentSheet</key> + <integer>0</integer> + <key>ExpandedCanvases</key> + <array> + <dict> + <key>name</key> + <string>Pyramid Router</string> + </dict> + </array> + <key>Frame</key> + <string>{{96, 20}, {1076, 1286}}</string> + <key>ListView</key> + <false/> + <key>OutlineWidth</key> + <integer>142</integer> + <key>RightSidebar</key> + <true/> + <key>ShowRuler</key> + <true/> + <key>Sidebar</key> + <true/> + <key>SidebarWidth</key> + <integer>120</integer> + <key>VisibleRegion</key> + <string>{{8, -10}, {532, 754.66666666666663}}</string> + <key>Zoom</key> + <real>1.5</real> + <key>ZoomValues</key> + <array> + <array> + <string>Pyramid Router</string> + <real>1.5</real> + <real>1</real> + </array> + </array> + </dict> +</dict> +</plist> diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png Binary files differnew file mode 100644 index 000000000..3c9f81158 --- /dev/null +++ b/docs/_static/pyramid_router.png diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg new file mode 100644 index 000000000..1537777c9 --- /dev/null +++ b/docs/_static/pyramid_router.svg @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="93 11 403 558" width="403pt" height="558pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2014-12-01 09:19Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Pyramid Router</title><rect fill="white" width="576" height="733"/><g><title>Layer 1</title><text transform="translate(229.04166 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".32226562" y="11" textLength="89.35547">Pyramid Router</tspan></text><rect x="104.416667" y="48" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="48" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 59.333334)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x=".088371277" y="10" textLength="129.51172">Obtain a root object from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="129.60009" y="10" textLength="55.561523">root factory</tspan></text><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 113.22222)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.557121" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="41.29931" y="10" textLength="108.393555">raverse the model graph</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.551262" y="22" textLength="39.458008">from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="69.00927" y="22" textLength="19.438477">root</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="88.447746" y="22" textLength="46.142578"> using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="134.590324" y="22" textLength="21.108398">path</tspan></text><rect x="104.416667" y="167.77778" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="167.77778" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 170.27778)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="53.428215" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="58.98974" y="10" textLength="38.36914">raversal</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="97.35888" y="10" textLength="34.46289"> locates</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="30.093254" y="22" textLength="16.6796875">the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="46.772942" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="82.334465" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="104.575676" y="22" textLength="50.581055">view name</tspan></text><rect x="104.416667" y="222" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="222" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 224.5)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3471603" y="10" textLength="46.704102">Look up a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="52.051262" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="113.19872" y="10" textLength="30.019531"> in the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="143.21825" y="10" textLength="36.68457">registry</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="179.90282" y="10" textLength="2.7783203"> </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.750969" y="22" textLength="43.364258">using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.115227" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="95.67675" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="117.91796" y="22" textLength="50.581055">view name</tspan></text><path d="M 202.04166 82.66667 C 202.04166 87.86648 202.04166 94.31586 202.04166 100.98615" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 142.55556 C 202.04166 147.75537 202.04166 154.20475 202.04166 160.87504" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 196.77778 C 202.04166 201.97759 202.04166 208.42697 202.04166 215.09726" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" fill="#ffff6c"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(161.29499 288.72223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.905763" y="10" textLength="6.669922">V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.399903" y="10" textLength="54.472656">iew callable </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="26.707032" y="22" textLength="30.585938">found?</tspan></text><path d="M 202.04166 251 C 202.04166 256.19981 202.04166 262.6492 202.04166 269.31949" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 295.22223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="10.89061" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.363266" y="10" textLength="59.42871">Not Found V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.616196" y="10" textLength="16.118164">iew</tspan></text><path d="M 265.20833 301.22223 C 297.4314 301.22223 294.2168 301.22223 320.5783 301.22223" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(288.07626 286.8889)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".33007812" y="11" textLength="15.339844">No</tspan></text><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" fill="#ffff6c"/><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(149.87354 380.12)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.495594" y="10" textLength="77.25586">Current user has </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x=".9462776" y="22" textLength="62.773438">authorization</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="63.719715" y="22" textLength="45.581055"> to invoke </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="14.26659" y="34" textLength="78.935547">the view callable?</tspan></text><path d="M 202.04165 326.72223 C 202.04165 332.1894 202.04165 344.2467 202.04165 353.54354" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(176.70832 334.24978)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="7.114258" y="11" textLength="12.673828">es</tspan></text><path d="M 202.04165 437.33333 C 202.04165 442.81278 202.04166 455.21977 202.04166 464.65762" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(180.11595 444)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="7.114258" y="11" textLength="12.673828">es</tspan></text><rect x="327.47912" y="384.38889" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="384.38889" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 392.88889)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.439926" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.912582" y="10" textLength="58.330078">Forbidden V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.06688" y="10" textLength="16.118164">iew</tspan></text><path d="M 280.85417 398.88889 C 312.9685 398.88889 296.55343 398.88889 320.57617 398.88889" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(296.21563 384.55555)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".33007812" y="11" textLength="15.339844">No</tspan></text><rect x="104.416667" y="471.55558" width="195.24998" height="29" fill="#ffff6c"/><rect x="104.416667" y="471.55558" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 474.05558)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="36.201653" y="10" textLength="48.920898">Invoke the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="85.12255" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="146.27001" y="10" textLength="2.7783203">,</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.100578" y="22" textLength="70.585938">which returns a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="105.686516" y="22" textLength="44.46289">response</tspan></text><rect x="104.416667" y="528.77776" width="195.24998" height="29" fill="#dfbeff"/><rect x="104.416667" y="528.77776" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 537.27776)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="45.65722" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="95.129875" y="10" textLength="44.46289">response</tspan></text><path d="M 202.04166 501.05558 C 202.04166 506.35088 202.04166 514.3792 202.04166 521.8749" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g></g></svg> diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index be7942502..000000000 --- a/docs/api.rst +++ /dev/null @@ -1,37 +0,0 @@ -API Documentation -================= - -Comprehensive reference material for every public API exposed by -:app:`Pyramid` is available within this chapter. The API -documentation is organized alphabetically by module name. - -.. toctree:: - :maxdepth: 1 - - api/authorization - api/authentication - api/chameleon_text - api/chameleon_zpt - api/config - api/events - api/exceptions - api/httpexceptions - api/i18n - api/interfaces - api/location - api/paster - api/registry - api/renderers - api/request - api/response - api/scripting - api/security - api/session - api/settings - api/testing - api/threadlocal - api/traversal - api/url - api/view - api/wsgi - diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 5d4dbd9e3..19d08618b 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -9,12 +9,24 @@ Authentication Policies .. automodule:: pyramid.authentication .. autoclass:: AuthTktAuthenticationPolicy - - .. autoclass:: RepozeWho1AuthenticationPolicy + :members: + :inherited-members: .. autoclass:: RemoteUserAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: SessionAuthenticationPolicy + :members: + :inherited-members: + + .. autoclass:: BasicAuthAuthenticationPolicy + :members: + :inherited-members: + + .. autoclass:: RepozeWho1AuthenticationPolicy + :members: + :inherited-members: Helper Classes ~~~~~~~~~~~~~~ diff --git a/docs/api/chameleon_text.rst b/docs/api/chameleon_text.rst deleted file mode 100644 index 494f5b464..000000000 --- a/docs/api/chameleon_text.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _chameleon_text_module: - -:mod:`pyramid.chameleon_text` ----------------------------------- - -.. automodule:: pyramid.chameleon_text - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will will work against template files which contain simple -``${Genshi}`` - style replacement markers. - -The API of :mod:`pyramid.chameleon_text` is identical to that of -:mod:`pyramid.chameleon_zpt`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_zpt` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render - - - diff --git a/docs/api/chameleon_zpt.rst b/docs/api/chameleon_zpt.rst deleted file mode 100644 index df9a36a56..000000000 --- a/docs/api/chameleon_zpt.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _chameleon_zpt_module: - -:mod:`pyramid.chameleon_zpt` -------------------------------- - -.. automodule:: pyramid.chameleon_zpt - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will work against files which supply template text which -matches the :term:`ZPT` specification. - -The API of :mod:`pyramid.chameleon_zpt` is identical to that of -:mod:`pyramid.chameleon_text`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_text` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render diff --git a/docs/api/compat.rst b/docs/api/compat.rst new file mode 100644 index 000000000..bb34f38e4 --- /dev/null +++ b/docs/api/compat.rst @@ -0,0 +1,156 @@ +.. _compat_module: + +:mod:`pyramid.compat` +---------------------- + +The ``pyramid.compat`` module provides platform and version compatibility for +Pyramid and its add-ons across Python platform and version differences. APIs +will be removed from this module over time as Pyramid ceases to support +systems which require compatibility imports. + +.. automodule:: pyramid.compat + + .. autofunction:: ascii_native_ + + .. attribute:: binary_type + + Binary type for this platform. For Python 3, it's ``bytes``. For + Python 2, it's ``str``. + + .. autofunction:: bytes_ + + .. attribute:: class_types + + Sequence of class types for this platform. For Python 3, it's + ``(type,)``. For Python 2, it's ``(type, types.ClassType)``. + + .. attribute:: configparser + + On Python 2, the ``ConfigParser`` module, on Python 3, the + ``configparser`` module. + + .. function:: escape(v) + + On Python 2, the ``cgi.escape`` function, on Python 3, the + ``html.escape`` function. + + .. function:: exec_(code, globs=None, locs=None) + + Exec code in a compatible way on both Python 2 and 3. + + .. attribute:: im_func + + On Python 2, the string value ``im_func``, on Python 3, the string + value ``__func__``. + + .. function:: input_(v) + + On Python 2, the ``raw_input`` function, on Python 3, the + ``input`` function. + + .. attribute:: integer_types + + Sequence of integer types for this platform. For Python 3, it's + ``(int,)``. For Python 2, it's ``(int, long)``. + + .. function:: is_nonstr_iter(v) + + Return ``True`` if ``v`` is a non-``str`` iterable on both Python 2 and + Python 3. + + .. function:: iteritems_(d) + + Return ``d.items()`` on Python 3, ``d.iteritems()`` on Python 2. + + .. function:: itervalues_(d) + + Return ``d.values()`` on Python 3, ``d.itervalues()`` on Python 2. + + .. function:: iterkeys_(d) + + Return ``d.keys()`` on Python 3, ``d.iterkeys()`` on Python 2. + + .. attribute:: long + + Long type for this platform. For Python 3, it's ``int``. For + Python 2, it's ``long``. + + .. function:: map_(v) + + Return ``list(map(v))`` on Python 3, ``map(v)`` on Python 2. + + .. attribute:: pickle + + ``cPickle`` module if it exists, ``pickle`` module otherwise. + + .. attribute:: PY3 + + ``True`` if running on Python 3, ``False`` otherwise. + + .. attribute:: PYPY + + ``True`` if running on PyPy, ``False`` otherwise. + + .. function:: reraise(tp, value, tb=None) + + Reraise an exception in a compatible way on both Python 2 and Python 3, + e.g. ``reraise(*sys.exc_info())``. + + .. attribute:: string_types + + Sequence of string types for this platform. For Python 3, it's + ``(str,)``. For Python 2, it's ``(basestring,)``. + + .. attribute:: SimpleCookie + + On Python 2, the ``Cookie.SimpleCookie`` class, on Python 3, the + ``http.cookies.SimpleCookie`` module. + + .. autofunction:: text_ + + .. attribute:: text_type + + Text type for this platform. For Python 3, it's ``str``. For Python + 2, it's ``unicode``. + + .. autofunction:: native_ + + .. attribute:: urlparse + + ``urlparse`` module on Python 2, ``urllib.parse`` module on Python 3. + + .. attribute:: url_quote + + ``urllib.quote`` function on Python 2, ``urllib.parse.quote`` function + on Python 3. + + .. attribute:: url_quote_plus + + ``urllib.quote_plus`` function on Python 2, ``urllib.parse.quote_plus`` + function on Python 3. + + .. attribute:: url_unquote + + ``urllib.unquote`` function on Python 2, ``urllib.parse.unquote`` + function on Python 3. + + .. attribute:: url_encode + + ``urllib.urlencode`` function on Python 2, ``urllib.parse.urlencode`` + function on Python 3. + + .. attribute:: url_open + + ``urllib2.urlopen`` function on Python 2, ``urllib.request.urlopen`` + function on Python 3. + + .. function:: url_unquote_text(v, encoding='utf-8', errors='replace') + + On Python 2, return ``url_unquote(v).decode(encoding(encoding, errors))``; + on Python 3, return the result of ``urllib.parse.unquote``. + + .. function:: url_unquote_native(v, encoding='utf-8', errors='replace') + + On Python 2, return ``native_(url_unquote_text_v, encoding, errors))``; + on Python 3, return the result of ``urllib.parse.unquote``. + diff --git a/docs/api/config.rst b/docs/api/config.rst index 2b9d7bcef..e083dbc68 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -1,86 +1,140 @@ .. _configuration_module: +.. role:: methodcategory + :class: methodcategory + :mod:`pyramid.config` --------------------- .. automodule:: pyramid.config - .. autoclass:: Configurator(registry=None, package=None, settings=None, root_factory=None, authentication_policy=None, authorization_policy=None, renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, session_factory=None, autocommit=False) +.. autoclass:: Configurator - .. attribute:: registry + :methodcategory:`Controlling Configuration State` - The :term:`application registry` which holds the configuration - associated with this configurator. + .. automethod:: commit + .. automethod:: begin + .. automethod:: end + .. automethod:: include + .. automethod:: make_wsgi_app() + .. automethod:: scan - .. automethod:: begin + :methodcategory:`Adding Routes and Views` - .. automethod:: end + .. automethod:: add_route + .. automethod:: add_static_view(name, path, cache_max_age=3600, permission=NO_PERMISSION_REQUIRED) + .. automethod:: add_view + .. automethod:: add_notfound_view + .. automethod:: add_forbidden_view - .. automethod:: hook_zca + :methodcategory:`Adding an Event Subscriber` - .. automethod:: unhook_zca + .. automethod:: add_subscriber - .. automethod:: get_settings + :methodcategory:`Using Security` - .. automethod:: commit + .. automethod:: set_authentication_policy + .. automethod:: set_authorization_policy + .. automethod:: set_default_permission + .. automethod:: add_permission - .. automethod:: action + :methodcategory:`Extending the Request Object` - .. automethod:: include + .. automethod:: add_request_method + .. automethod:: set_request_property - .. automethod:: add_directive + :methodcategory:`Using I18N` - .. automethod:: with_package + .. automethod:: add_translation_dirs + .. automethod:: set_locale_negotiator - .. automethod:: maybe_dotted + :methodcategory:`Overriding Assets` - .. automethod:: absolute_asset_spec + .. automethod:: override_asset(to_override, override_with) - .. automethod:: setup_registry(settings=None, root_factory=None, authentication_policy=None, renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None) + :methodcategory:`Getting and Adding Settings` + + .. automethod:: add_settings + .. automethod:: get_settings - .. automethod:: add_renderer(name, factory) + :methodcategory:`Hooking Pyramid Behavior` - .. automethod:: add_route + .. automethod:: add_renderer + .. automethod:: add_resource_url_adapter + .. automethod:: add_response_adapter + .. automethod:: add_traverser + .. automethod:: add_tween + .. automethod:: add_route_predicate + .. automethod:: add_view_predicate + .. automethod:: add_view_deriver + .. automethod:: set_request_factory + .. automethod:: set_root_factory + .. automethod:: set_session_factory + .. automethod:: set_view_mapper - .. automethod:: add_static_view(name, path, cache_max_age=3600, permission='__no_permission_required__') + :methodcategory:`Extension Author APIs` - .. automethod:: add_settings + .. automethod:: action + .. automethod:: add_directive + .. automethod:: with_package + .. automethod:: derive_view - .. automethod:: add_subscriber + :methodcategory:`Utility Methods` - .. automethod:: add_translation_dirs + .. automethod:: absolute_asset_spec + .. automethod:: maybe_dotted - .. automethod:: add_view + :methodcategory:`ZCA-Related APIs` - .. automethod:: derive_view + .. automethod:: hook_zca + .. automethod:: unhook_zca + .. automethod:: setup_registry - .. automethod:: make_wsgi_app() + :methodcategory:`Testing Helper APIs` - .. automethod:: override_asset(to_override, override_with) + .. automethod:: testing_add_renderer + .. automethod:: testing_add_subscriber + .. automethod:: testing_resources + .. automethod:: testing_securitypolicy - .. automethod:: scan + :methodcategory:`Attributes` - .. automethod:: set_forbidden_view + .. attribute:: introspectable - .. automethod:: set_notfound_view + A shortcut attribute which points to the + :class:`pyramid.registry.Introspectable` class (used during + directives to provide introspection to actions). - .. automethod:: set_locale_negotiator + .. versionadded:: 1.3 - .. automethod:: set_default_permission + .. attribute:: introspector - .. automethod:: set_session_factory + The :term:`introspector` related to this configuration. It is an + instance implementing the :class:`pyramid.interfaces.IIntrospector` + interface. - .. automethod:: set_request_factory + .. versionadded:: 1.3 - .. automethod:: set_renderer_globals_factory + .. attribute:: registry - .. automethod:: set_view_mapper + The :term:`application registry` which holds the configuration + associated with this configurator. - .. automethod:: testing_securitypolicy +.. attribute:: global_registries - .. automethod:: testing_resources + The set of registries that have been created for :app:`Pyramid` + applications, one for each call to + :meth:`pyramid.config.Configurator.make_wsgi_app` in the current + process. The object itself supports iteration and has a ``last`` property + containing the last registry loaded. - .. automethod:: testing_add_subscriber + The registries contained in this object are stored as weakrefs, thus they + will only exist for the lifetime of the actual applications for which they + are being used. - .. automethod:: testing_add_renderer +.. autoclass:: not_ +.. attribute:: PHASE0_CONFIG +.. attribute:: PHASE1_CONFIG +.. attribute:: PHASE2_CONFIG +.. attribute:: PHASE3_CONFIG diff --git a/docs/api/decorator.rst b/docs/api/decorator.rst new file mode 100644 index 000000000..35d9131df --- /dev/null +++ b/docs/api/decorator.rst @@ -0,0 +1,9 @@ +.. _decorator_module: + +:mod:`pyramid.decorator` +-------------------------- + +.. automodule:: pyramid.decorator + +.. autofunction:: reify + diff --git a/docs/api/events.rst b/docs/api/events.rst index 59657a820..0a8463740 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -21,10 +21,21 @@ Event Types .. autoclass:: ContextFound +.. autoclass:: BeforeTraversal + .. autoclass:: NewResponse .. autoclass:: BeforeRender :members: + :inherited-members: + :exclude-members: update + + .. method:: update(E, **F) + + Update D from dict/iterable E and F. If E has a .keys() method, does: + for k in E: D[k] = E[k] If E lacks .keys() method, does: for (k, v) in + E: D[k] = v. In either case, this is followed by: for k in F: D[k] = + F[k]. See :ref:`events_chapter` for more information about how to register code which subscribes to these events. diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst index 1dfbf46fd..cb411458d 100644 --- a/docs/api/exceptions.rst +++ b/docs/api/exceptions.rst @@ -5,10 +5,16 @@ .. automodule:: pyramid.exceptions - .. autoclass:: Forbidden + .. autoexception:: BadCSRFOrigin - .. autoclass:: NotFound + .. autoexception:: BadCSRFToken - .. autoclass:: ConfigurationError + .. autoexception:: PredicateMismatch - .. autoclass:: URLDecodeError + .. autoexception:: Forbidden + + .. autoexception:: NotFound + + .. autoexception:: ConfigurationError + + .. autoexception:: URLDecodeError diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 57ca8092c..d4cf97f1d 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -7,92 +7,102 @@ .. attribute:: status_map - A mapping of integer status code to exception class (eg. the - integer "401" maps to - :class:`pyramid.httpexceptions.HTTPUnauthorized`). + A mapping of integer status code to HTTP exception class (eg. the integer + "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). All + mapped exception classes are children of :class:`pyramid.httpexceptions`, - .. autoclass:: HTTPException + .. autofunction:: exception_response - .. autoclass:: HTTPOk + .. autoexception:: HTTPException - .. autoclass:: HTTPRedirection + .. autoexception:: HTTPOk - .. autoclass:: HTTPError + .. autoexception:: HTTPRedirection - .. autoclass:: HTTPClientError + .. autoexception:: HTTPError - .. autoclass:: HTTPServerError + .. autoexception:: HTTPClientError - .. autoclass:: HTTPCreated + .. autoexception:: HTTPServerError - .. autoclass:: HTTPAccepted + .. autoexception:: HTTPCreated - .. autoclass:: HTTPNonAuthoritativeInformation + .. autoexception:: HTTPAccepted - .. autoclass:: HTTPNoContent + .. autoexception:: HTTPNonAuthoritativeInformation - .. autoclass:: HTTPResetContent + .. autoexception:: HTTPNoContent - .. autoclass:: HTTPPartialContent + .. autoexception:: HTTPResetContent - .. autoclass:: HTTPMultipleChoices + .. autoexception:: HTTPPartialContent - .. autoclass:: HTTPMovedPermanently + .. autoexception:: HTTPMultipleChoices - .. autoclass:: HTTPFound + .. autoexception:: HTTPMovedPermanently - .. autoclass:: HTTPSeeOther + .. autoexception:: HTTPFound - .. autoclass:: HTTPNotModified + .. autoexception:: HTTPSeeOther - .. autoclass:: HTTPUseProxy + .. autoexception:: HTTPNotModified - .. autoclass:: HTTPTemporaryRedirect + .. autoexception:: HTTPUseProxy - .. autoclass:: HTTPBadRequest + .. autoexception:: HTTPTemporaryRedirect - .. autoclass:: HTTPUnauthorized + .. autoexception:: HTTPBadRequest - .. autoclass:: HTTPPaymentRequired + .. autoexception:: HTTPUnauthorized - .. autoclass:: HTTPForbidden + .. autoexception:: HTTPPaymentRequired - .. autoclass:: HTTPNotFound + .. autoexception:: HTTPForbidden - .. autoclass:: HTTPMethodNotAllowed + .. autoexception:: HTTPNotFound - .. autoclass:: HTTPNotAcceptable + .. autoexception:: HTTPMethodNotAllowed - .. autoclass:: HTTPProxyAuthenticationRequired + .. autoexception:: HTTPNotAcceptable - .. autoclass:: HTTPRequestTimeout + .. autoexception:: HTTPProxyAuthenticationRequired - .. autoclass:: HTTPConflict + .. autoexception:: HTTPRequestTimeout - .. autoclass:: HTTPGone + .. autoexception:: HTTPConflict - .. autoclass:: HTTPLengthRequired + .. autoexception:: HTTPGone - .. autoclass:: HTTPPreconditionFailed + .. autoexception:: HTTPLengthRequired - .. autoclass:: HTTPRequestEntityTooLarge + .. autoexception:: HTTPPreconditionFailed - .. autoclass:: HTTPRequestURITooLong + .. autoexception:: HTTPRequestEntityTooLarge - .. autoclass:: HTTPUnsupportedMediaType + .. autoexception:: HTTPRequestURITooLong - .. autoclass:: HTTPRequestRangeNotSatisfiable + .. autoexception:: HTTPUnsupportedMediaType - .. autoclass:: HTTPExpectationFailed + .. autoexception:: HTTPRequestRangeNotSatisfiable - .. autoclass:: HTTPInternalServerError + .. autoexception:: HTTPExpectationFailed - .. autoclass:: HTTPNotImplemented + .. autoexception:: HTTPUnprocessableEntity - .. autoclass:: HTTPBadGateway + .. autoexception:: HTTPLocked - .. autoclass:: HTTPServiceUnavailable + .. autoexception:: HTTPFailedDependency - .. autoclass:: HTTPGatewayTimeout + .. autoexception:: HTTPInternalServerError - .. autoclass:: HTTPVersionNotSupported + .. autoexception:: HTTPNotImplemented + + .. autoexception:: HTTPBadGateway + + .. autoexception:: HTTPServiceUnavailable + + .. autoexception:: HTTPGatewayTimeout + + .. autoexception:: HTTPVersionNotSupported + + .. autoexception:: HTTPInsufficientStorage diff --git a/docs/api/i18n.rst b/docs/api/i18n.rst index 53e8c8a9b..3b9abbc1d 100644 --- a/docs/api/i18n.rst +++ b/docs/api/i18n.rst @@ -7,7 +7,7 @@ .. autoclass:: TranslationString - .. autoclass:: TranslationStringFactory + .. autofunction:: TranslationStringFactory .. autoclass:: Localizer :members: diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..cb38aa0b2 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,12 @@ +.. _html_api_documentation: + +API Documentation +================= + +Comprehensive reference material for every public API exposed by :app:`Pyramid`: + +.. toctree:: + :maxdepth: 1 + :glob: + + * diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index ac282fbcc..272820a91 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -9,14 +9,22 @@ Event-Related Interfaces ++++++++++++++++++++++++ .. autointerface:: IApplicationCreated + :members: .. autointerface:: INewRequest + :members: .. autointerface:: IContextFound + :members: + + .. autointerface:: IBeforeTraversal + :members: .. autointerface:: INewResponse + :members: .. autointerface:: IBeforeRender + :members: Other Interfaces ++++++++++++++++ @@ -45,7 +53,13 @@ Other Interfaces .. autointerface:: IRendererInfo :members: - .. autointerface:: ITemplateRenderer + .. autointerface:: IRendererFactory + :members: + + .. autointerface:: IRenderer + :members: + + .. autointerface:: IResponseFactory :members: .. autointerface:: IViewMapperFactory @@ -54,7 +68,35 @@ Other Interfaces .. autointerface:: IViewMapper :members: + .. autointerface:: IDict + :members: + .. autointerface:: IMultiDict :members: + .. autointerface:: IResponse + :members: + .. autointerface:: IIntrospectable + :members: + + .. autointerface:: IIntrospector + :members: + + .. autointerface:: IActionInfo + :members: + + .. autointerface:: IAssetDescriptor + :members: + + .. autointerface:: IResourceURL + :members: + + .. autointerface:: ICacheBuster + :members: + + .. autointerface:: IViewDeriver + :members: + + .. autointerface:: IViewDeriverInfo + :members: diff --git a/docs/api/paster.rst b/docs/api/paster.rst index 9ecfa3d9c..27bc81a1f 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -3,11 +3,12 @@ :mod:`pyramid.paster` --------------------------- -.. module:: pyramid.paster +.. automodule:: pyramid.paster -.. function:: get_app(config_file, name) + .. autofunction:: bootstrap - Return the WSGI application named ``name`` in the PasteDeploy - config file ``config_file``. + .. autofunction:: get_app(config_uri, name=None, options=None) - + .. autofunction:: get_appsettings(config_uri, name=None, options=None) + + .. autofunction:: setup_logging(config_uri, global_conf=None) diff --git a/docs/api/path.rst b/docs/api/path.rst new file mode 100644 index 000000000..814fc47d5 --- /dev/null +++ b/docs/api/path.rst @@ -0,0 +1,19 @@ +.. _path_module: + +:mod:`pyramid.path` +--------------------------- + +.. automodule:: pyramid.path + + .. attribute:: CALLER_PACKAGE + + A constant used by the constructor of + :class:`pyramid.path.DottedNameResolver` and + :class:`pyramid.path.AssetResolver`. + + .. autoclass:: DottedNameResolver + :members: + + .. autoclass:: AssetResolver + :members: + diff --git a/docs/api/registry.rst b/docs/api/registry.rst index 4d327370a..57a80b3f5 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -14,3 +14,62 @@ accessed as ``request.registry.settings`` or ``config.registry.settings`` in a typical Pyramid application. + .. attribute:: package_name + + .. versionadded:: 1.6 + + When a registry is set up (or created) by a :term:`Configurator`, this + attribute will be the shortcut for + :attr:`pyramid.config.Configurator.package_name`. + + This attribute is often accessed as ``request.registry.package_name`` or + ``config.registry.package_name`` or ``config.package_name`` + in a typical Pyramid application. + + .. attribute:: introspector + + .. versionadded:: 1.3 + + When a registry is set up (or created) by a :term:`Configurator`, the + registry will be decorated with an instance named ``introspector`` + implementing the :class:`pyramid.interfaces.IIntrospector` interface. + + .. seealso:: + + See also :attr:`pyramid.config.Configurator.introspector`. + + When a registry is created "by hand", however, this attribute will not + exist until set up by a configurator. + + This attribute is often accessed as ``request.registry.introspector`` in + a typical Pyramid application. + + .. method:: notify(*events) + + Fire one or more events. All event subscribers to the event(s) + will be notified. The subscribers will be called synchronously. + This method is often accessed as ``request.registry.notify`` + in Pyramid applications to fire custom events. See + :ref:`custom_events` for more information. + + +.. class:: Introspectable + + .. versionadded:: 1.3 + + The default implementation of the interface + :class:`pyramid.interfaces.IIntrospectable` used by framework exenders. + An instance of this class is created when + :attr:`pyramid.config.Configurator.introspectable` is called. + +.. autoclass:: Deferred + + .. versionadded:: 1.4 + +.. autofunction:: undefer + + .. versionadded:: 1.4 + +.. autoclass:: predvalseq + + .. versionadded:: 1.4 diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 459639a46..0caca02b4 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -11,3 +11,20 @@ .. autofunction:: render_to_response +.. autoclass:: JSON + + .. automethod:: add_adapter + +.. autoclass:: JSONP + + .. automethod:: add_adapter + +.. attribute:: null_renderer + + An object that can be used in advanced integration cases as input to the + view configuration ``renderer=`` argument. When the null renderer is used + as a view renderer argument, Pyramid avoids converting the view callable + result into a Response object. This is useful if you want to reuse the + view configuration and lookup machinery outside the context of its use by + the Pyramid router. + diff --git a/docs/api/request.rst b/docs/api/request.rst index 8cb424658..52bf50078 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -8,6 +8,13 @@ .. autoclass:: Request :members: :inherited-members: + :exclude-members: add_response_callback, add_finished_callback, + route_url, route_path, current_route_url, + current_route_path, static_url, static_path, + model_url, resource_url, resource_path, set_property, + effective_principals, authenticated_userid, + unauthenticated_userid, has_permission, + invoke_exception_view .. attribute:: context @@ -85,6 +92,17 @@ of ``request.exception`` will be ``None`` within response and finished callbacks. + .. attribute:: exc_info + + If an exception was raised by a :term:`root factory` or a :term:`view + callable`, or at various other points where :app:`Pyramid` executes + user-defined code during the processing of a request, result of + ``sys.exc_info()`` will be available as the ``exc_info`` attribute of + the request within a :term:`exception view`, a :term:`response callback` + or a :term:`finished callback`. If no exception occurred, the value of + ``request.exc_info`` will be ``None`` within response and finished + callbacks. + .. attribute:: response This attribute is actually a "reified" property which returns an @@ -107,7 +125,9 @@ return {'text':'Value that will be used by the renderer'} Mutations to this response object will be preserved in the response sent - to the client after rendering. + to the client after rendering. For more information about using + ``request.response`` in conjunction with a renderer, see + :ref:`request_response_attr`. Non-renderer code can also make use of request.response instead of creating a response "by hand". For example, in view code:: @@ -128,10 +148,6 @@ ``request.session`` attribute will cause a :class:`pyramid.exceptions.ConfigurationError` to be raised. - .. attribute:: tmpl_context - - The template context for Pylons-style applications. - .. attribute:: matchdict If a :term:`route` has matched during this request, this attribute will @@ -143,11 +159,111 @@ .. attribute:: matched_route If a :term:`route` has matched during this request, this attribute will - be an obect representing the route matched by the URL pattern + be an object representing the route matched by the URL pattern associated with the route. If a route has not matched during this request, the value of this attribute will be ``None``. See :ref:`matched_route`. + .. attribute:: authenticated_userid + + .. versionadded:: 1.5 + + A property which returns the :term:`userid` of the currently + authenticated user or ``None`` if there is no :term:`authentication + policy` in effect or there is no currently authenticated user. This + differs from :attr:`~pyramid.request.Request.unauthenticated_userid`, + because the effective authentication policy will have ensured that a + record associated with the :term:`userid` exists in persistent storage; + if it has not, this value will be ``None``. + + .. attribute:: unauthenticated_userid + + .. versionadded:: 1.5 + + A property which returns a value which represents the *claimed* (not + verified) :term:`userid` of the credentials present in the + request. ``None`` if there is no :term:`authentication policy` in effect + or there is no user data associated with the current request. This + differs from :attr:`~pyramid.request.Request.authenticated_userid`, + because the effective authentication policy will not ensure that a + record associated with the :term:`userid` exists in persistent storage. + Even if the :term:`userid` does not exist in persistent storage, this + value will be the value of the :term:`userid` *claimed* by the request + data. + + .. attribute:: effective_principals + + .. versionadded:: 1.5 + + A property which returns the list of 'effective' :term:`principal` + identifiers for this request. This list typically includes the + :term:`userid` of the currently authenticated user if a user is + currently authenticated, but this depends on the + :term:`authentication policy` in effect. If no :term:`authentication + policy` is in effect, this will return a sequence containing only the + :attr:`pyramid.security.Everyone` principal. + + .. method:: invoke_subrequest(request, use_tweens=False) + + .. versionadded:: 1.4a1 + + Obtain a response object from the Pyramid application based on + information in the ``request`` object provided. The ``request`` object + must be an object that implements the Pyramid request interface (such + as a :class:`pyramid.request.Request` instance). If ``use_tweens`` is + ``True``, the request will be sent to the :term:`tween` in the tween + stack closest to the request ingress. If ``use_tweens`` is ``False``, + the request will be sent to the main router handler, and no tweens will + be invoked. + + This function also: + + - manages the threadlocal stack (so that + :func:`~pyramid.threadlocal.get_current_request` and + :func:`~pyramid.threadlocal.get_current_registry` work during a + request) + + - Adds a ``registry`` attribute (the current Pyramid registry) and a + ``invoke_subrequest`` attribute (a callable) to the request object it's + handed. + + - sets request extensions (such as those added via + :meth:`~pyramid.config.Configurator.add_request_method` or + :meth:`~pyramid.config.Configurator.set_request_property`) on the + request it's passed. + + - causes a :class:`~pyramid.events.NewRequest` event to be sent at the + beginning of request processing. + + - causes a :class:`~pyramid.events.ContextFound` event to be sent + when a context resource is found. + + - Ensures that the user implied by the request passed has the necessary + authorization to invoke view callable before calling it. + + - Calls any :term:`response callback` functions defined within the + request's lifetime if a response is obtained from the Pyramid + application. + + - causes a :class:`~pyramid.events.NewResponse` event to be sent if a + response is obtained. + + - Calls any :term:`finished callback` functions defined within the + request's lifetime. + + ``invoke_subrequest`` isn't *actually* a method of the Request object; + it's a callable added when the Pyramid router is invoked, or when a + subrequest is invoked. This means that it's not available for use on a + request provided by e.g. the ``pshell`` environment. + + .. seealso:: + + See also :ref:`subrequest_chapter`. + + .. automethod:: invoke_exception_view + + .. automethod:: has_permission + .. automethod:: add_response_callback .. automethod:: add_finished_callback @@ -156,26 +272,99 @@ .. automethod:: route_path - .. automethod:: resource_url + .. automethod:: current_route_url + + .. automethod:: current_route_path .. automethod:: static_url - .. attribute:: response_* + .. automethod:: static_path + + .. automethod:: resource_url + + .. automethod:: resource_path + + .. attribute:: json_body + + This property will return the JSON-decoded variant of the request + body. If the request body is not well-formed JSON, or there is no + body associated with this request, this property will raise an + exception. + + .. seealso:: + + See also :ref:`request_json_body`. + + .. method:: set_property(callable, name=None, reify=False) + + Add a callable or a property descriptor to the request instance. + + Properties, unlike attributes, are lazily evaluated by executing + an underlying callable when accessed. They can be useful for + adding features to an object without any cost if those features + go unused. + + A property may also be reified via the + :class:`pyramid.decorator.reify` decorator by setting + ``reify=True``, allowing the result of the evaluation to be + cached. Thus the value of the property is only computed once for + the lifetime of the object. + + ``callable`` can either be a callable that accepts the request as + its single positional parameter, or it can be a property + descriptor. + + If the ``callable`` is a property descriptor a ``ValueError`` + will be raised if ``name`` is ``None`` or ``reify`` is ``True``. + + If ``name`` is None, the name of the property will be computed + from the name of the ``callable``. + + .. code-block:: python + :linenos: + + def _connect(request): + conn = request.registry.dbsession() + def cleanup(request): + # since version 1.5, request.exception is no + # longer eagerly cleared + if request.exception is not None: + conn.rollback() + else: + conn.commit() + conn.close() + request.add_finished_callback(cleanup) + return conn + + @subscriber(NewRequest) + def new_request(event): + request = event.request + request.set_property(_connect, 'db', reify=True) + + The subscriber doesn't actually connect to the database, it just + provides the API which, when accessed via ``request.db``, will + create the connection. Thanks to reify, only one connection is + made per-request even if ``request.db`` is accessed many times. + + This pattern provides a way to augment the ``request`` object + without having to subclass it, which can be useful for extension + authors. + + .. versionadded:: 1.3 + + .. attribute:: localizer + + A :term:`localizer` which will use the current locale name to + translate values. + + .. versionadded:: 1.5 + + .. attribute:: locale_name - .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are - deprecated. Assigning to one will cause a deprecation warning to be - emitted. Instead of assigning ``response_*`` attributes to the - request, use API of the the :attr:`pyramid.request.Request.response` - object (exposed to view code as ``request.response``) to influence - response behavior. + The locale name of the current request as computed by the + :term:`locale negotiator`. - You can set attributes on a :class:`pyramid.request.Request` which will - influence the behavor of *rendered* responses (views which use a - :term:`renderer` and which don't directly return a response). These - attributes begin with ``response_``, such as ``response_headerlist``. If - you need to influence response values from a view that uses a renderer - (such as the status code, a header, the content type, etc) see, - :ref:`response_prefixed_attrs`. + .. versionadded:: 1.5 .. note:: @@ -183,3 +372,4 @@ that used as ``request.GET``, ``request.POST``, and ``request.params``), see :class:`pyramid.interfaces.IMultiDict`. +.. autofunction:: apply_request_extensions(request) diff --git a/docs/api/response.rst b/docs/api/response.rst index c545b4977..52978a126 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -8,3 +8,14 @@ .. autoclass:: Response :members: :inherited-members: + +.. autoclass:: FileResponse + :members: + +.. autoclass:: FileIter + +Functions +~~~~~~~~~ + +.. autofunction:: response_adapter + diff --git a/docs/api/scaffolds.rst b/docs/api/scaffolds.rst new file mode 100644 index 000000000..827962e19 --- /dev/null +++ b/docs/api/scaffolds.rst @@ -0,0 +1,13 @@ +.. _scaffolds_module: + +:mod:`pyramid.scaffolds` +------------------------ + +.. automodule:: pyramid.scaffolds + + .. autoclass:: pyramid.scaffolds.Template + :members: + + .. autoclass:: pyramid.scaffolds.PyramidTemplate + :members: + diff --git a/docs/api/scripting.rst b/docs/api/scripting.rst index 9d5bc2e58..51bd3c7a0 100644 --- a/docs/api/scripting.rst +++ b/docs/api/scripting.rst @@ -7,3 +7,5 @@ .. autofunction:: get_root + .. autofunction:: prepare + diff --git a/docs/api/security.rst b/docs/api/security.rst index de249355d..88086dbbf 100644 --- a/docs/api/security.rst +++ b/docs/api/security.rst @@ -16,7 +16,7 @@ Authentication API Functions .. autofunction:: forget -.. autofunction:: remember +.. autofunction:: remember(request, userid, **kwargs) Authorization API Functions --------------------------- @@ -57,6 +57,14 @@ Constants last ACE in an ACL in systems that use an "inheriting" security policy, representing the concept "don't inherit any other ACEs". +.. attribute:: NO_PERMISSION_REQUIRED + + A special permission which indicates that the view should always + be executable by entirely anonymous users, regardless of the + default permission, bypassing any :term:`authorization policy` + that may be in effect. Its actual value is the string + '__no_permission_required__'. + Return Values ------------- @@ -64,13 +72,13 @@ Return Values The ACE "action" (the first element in an ACE e.g. ``(Allow, Everyone, 'read')`` that means allow access. A sequence of ACEs makes up an - ACL. It is a string, and it's actual value is "Allow". + ACL. It is a string, and its actual value is "Allow". .. attribute:: Deny The ACE "action" (the first element in an ACE e.g. ``(Deny, 'george', 'read')`` that means deny access. A sequence of ACEs - makes up an ACL. It is a string, and it's actual value is "Deny". + makes up an ACL. It is a string, and its actual value is "Deny". .. autoclass:: ACLDenied :members: diff --git a/docs/api/session.rst b/docs/api/session.rst index 44b4bd860..56c4f52d7 100644 --- a/docs/api/session.rst +++ b/docs/api/session.rst @@ -5,10 +5,19 @@ .. automodule:: pyramid.session - .. autofunction:: UnencryptedCookieSessionFactoryConfig - .. autofunction:: signed_serialize .. autofunction:: signed_deserialize + .. autofunction:: check_csrf_origin + + .. autofunction:: check_csrf_token + + .. autofunction:: SignedCookieSessionFactory + + .. autofunction:: UnencryptedCookieSessionFactoryConfig + + .. autofunction:: BaseCookieSessionFactory + + .. autoclass:: PickleSerializer diff --git a/docs/api/settings.rst b/docs/api/settings.rst index ac1cd3f9c..cd802e138 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -5,8 +5,8 @@ .. automodule:: pyramid.settings - .. autofunction:: get_settings - .. autofunction:: asbool + .. autofunction:: aslist + diff --git a/docs/api/static.rst b/docs/api/static.rst new file mode 100644 index 000000000..f3727e197 --- /dev/null +++ b/docs/api/static.rst @@ -0,0 +1,19 @@ +.. _static_module: + +:mod:`pyramid.static` +--------------------- + +.. automodule:: pyramid.static + + .. autoclass:: static_view + :members: + :inherited-members: + + .. autoclass:: ManifestCacheBuster + :members: + + .. autoclass:: QueryStringCacheBuster + :members: + + .. autoclass:: QueryStringConstantCacheBuster + :members: diff --git a/docs/api/testing.rst b/docs/api/testing.rst index f388dc263..1366a1795 100644 --- a/docs/api/testing.rst +++ b/docs/api/testing.rst @@ -9,6 +9,8 @@ .. autofunction:: tearDown + .. autofunction:: testConfig(registry=None, request=None, hook_zca=True, autocommit=True, settings=None) + .. autofunction:: cleanUp .. autoclass:: DummyResource diff --git a/docs/api/tweens.rst b/docs/api/tweens.rst new file mode 100644 index 000000000..ddacd2cde --- /dev/null +++ b/docs/api/tweens.rst @@ -0,0 +1,25 @@ +.. _tweens_module: + +:mod:`pyramid.tweens` +--------------------- + +.. automodule:: pyramid.tweens + + .. autofunction:: excview_tween_factory + + .. attribute:: MAIN + + Constant representing the main Pyramid handling function, for use in + ``under`` and ``over`` arguments to + :meth:`pyramid.config.Configurator.add_tween`. + + .. attribute:: INGRESS + + Constant representing the request ingress, for use in ``under`` and + ``over`` arguments to :meth:`pyramid.config.Configurator.add_tween`. + + .. attribute:: EXCVIEW + + Constant representing the exception view tween, for use in ``under`` + and ``over`` arguments to + :meth:`pyramid.config.Configurator.add_tween`. diff --git a/docs/api/url.rst b/docs/api/url.rst index 01be76283..131d85806 100644 --- a/docs/api/url.rst +++ b/docs/api/url.rst @@ -13,7 +13,11 @@ .. autofunction:: route_path + .. autofunction:: current_route_path + .. autofunction:: static_url + .. autofunction:: static_path + .. autofunction:: urlencode diff --git a/docs/api/view.rst b/docs/api/view.rst index 4dddea25f..d8e429552 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -11,16 +11,16 @@ .. autofunction:: render_view - .. autofunction:: is_response - .. autoclass:: view_config :members: - .. autoclass:: static + .. autoclass:: view_defaults + :members: + + .. autoclass:: notfound_view_config :members: - :inherited-members: - .. autofunction:: append_slash_notfound_view(context, request) + .. autoclass:: forbidden_view_config + :members: - .. autoclass:: AppendSlashNotFoundViewFactory diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst new file mode 100644 index 000000000..2a141501e --- /dev/null +++ b/docs/api/viewderivers.rst @@ -0,0 +1,17 @@ +.. _viewderivers_module: + +:mod:`pyramid.viewderivers` +--------------------------- + +.. automodule:: pyramid.viewderivers + + .. attribute:: INGRESS + + Constant representing the request ingress, for use in ``under`` + arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + + .. attribute:: VIEW + + Constant representing the :term:`view callable` at the end of the view + pipeline, for use in ``over`` arguments to + :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/authorintro.rst b/docs/authorintro.rst index fb34f1d34..ebc6bcff8 100644 --- a/docs/authorintro.rst +++ b/docs/authorintro.rst @@ -2,7 +2,7 @@ Author Introduction ===================== -Welcome to "The :app:`Pyramid` Web Application Framework". In this +Welcome to "The :app:`Pyramid` Web Framework". In this introduction, I'll describe the audience for this book, I'll describe the book content, I'll provide some context regarding the genesis of :app:`Pyramid`, and I'll thank some important people. @@ -73,7 +73,7 @@ This book is divided into three major parts: concepts in terms of the sample. You should read the tutorials if you want a guided tour of :app:`Pyramid`. -:ref:`api_reference` +:ref:`api_documentation` Comprehensive reference material for every public API exposed by :app:`Pyramid`. The API documentation is organized @@ -165,6 +165,7 @@ others' technology. single: Shipman, John single: Beelby, Chris single: Paez, Patricio + single: Merickel, Michael Thanks ====== @@ -179,8 +180,9 @@ software which it details would exist: Paul Everitt, Tres Seaver, Andrew Sawyers, Malthe Borch, Carlos de la Guardia, Chris Rossi, Shane Hathaway, Daniel Holth, Wichert Akkerman, Georg Brandl, Blaise Laflamme, Ben Bangert, Casey Duncan, Hugues Laflamme, Mike Orr, John Shipman, Chris Beelby, Patricio -Paez, Simon Oram, Nat Hardwick, Ian Bicking, Jim Fulton, Tom Moroz of the -Open Society Institute, and Todd Koym of Environmental Health Sciences. +Paez, Simon Oram, Nat Hardwick, Ian Bicking, Jim Fulton, Michael Merickel, +Tom Moroz of the Open Society Institute, and Todd Koym of Environmental +Health Sciences. Thanks to Guido van Rossum and Tim Peters for Python. diff --git a/docs/changes.rst b/docs/changes.rst index 6294123ed..fdeaf1e99 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,3 +1,5 @@ +.. _changelog: + :app:`Pyramid` Change History ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index a610351ff..518f7e784 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,13 +11,17 @@ # All configuration values have a default value; values that are commented out # serve to show the default value. -import sys, os +import sys +import os import datetime import inspect import warnings warnings.simplefilter('ignore', DeprecationWarning) +import pkg_resources +import pylons_sphinx_themes + # skip raw nodes from sphinx.writers.text import TextTranslator from sphinx.writers.latex import LaTeXTranslator @@ -25,6 +29,7 @@ from sphinx.writers.latex import LaTeXTranslator from docutils import nodes from docutils import utils + def raw(*arg): raise nodes.SkipNode TextTranslator.visit_raw = raw @@ -38,20 +43,6 @@ LaTeXTranslator.depart_inline = nothing book = os.environ.get('BOOK') -# If your extensions are in another directory, add it here. If the directory -# is relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -parent = os.path.dirname(os.path.dirname(__file__)) -sys.path.append(os.path.abspath(parent)) -wd = os.getcwd() -os.chdir(parent) -os.system('%s setup.py test -q' % sys.executable) -os.chdir(wd) - -for item in os.listdir(parent): - if item.endswith('.egg'): - sys.path.append(os.path.join(parent, item)) - # General configuration # --------------------- @@ -61,20 +52,35 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'repoze.sphinx.autointerface', -# 'sphinx.ext.intersphinx' + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinxcontrib.programoutput', + # enable pylons_sphinx_latesturl when this branch is no longer "latest" + # 'pylons_sphinx_latesturl', ] -# Looks for objects in other Pyramid projects -## intersphinx_mapping = { -## 'cookbook': -## ('http://docs.pylonsproject.org/projects/pyramid_cookbook/dev/', None), -## 'handlers': -## ('http://docs.pylonsproject.org/projects/pyramid_handlers/dev/', None), -## 'zcml': -## ('http://docs.pylonsproject.org/projects/pyramid_zcml/dev/', None), -## 'jinja2': -## ('http://docs.pylonsproject.org/projects/pyramid_jinja2/dev/', None), -## } +# Looks for objects in external projects +intersphinx_mapping = { + 'colander': ('http://docs.pylonsproject.org/projects/colander/en/latest', None), + 'cookbook': ('http://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/', None), + 'deform': ('http://docs.pylonsproject.org/projects/deform/en/latest', None), + 'jinja2': ('http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/', None), + 'pylonswebframework': ('http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/', None), + 'python': ('https://docs.python.org/3', None), + 'pytest': ('http://pytest.org/latest/', None), + 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), + 'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None), + 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), + 'tstring': ('http://docs.pylonsproject.org/projects/translationstring/en/latest', None), + 'tutorials': ('http://docs.pylonsproject.org/projects/pyramid-tutorials/en/latest/', None), + 'venusian': ('http://docs.pylonsproject.org/projects/venusian/en/latest', None), + 'webob': ('http://docs.webob.org/en/latest', None), + 'webtest': ('http://webtest.pythonpaste.org/en/latest', None), + 'who': ('http://repozewho.readthedocs.org/en/latest', None), + 'zcml': ('http://docs.pylonsproject.org/projects/pyramid-zcml/en/latest', None), + 'zcomponent': ('http://docs.zope.org/zope.component', None), +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -86,14 +92,16 @@ source_suffix = '.rst' master_doc = 'index' # General substitutions. -project = 'The Pyramid Web Application Development Framework' -copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year +project = 'The Pyramid Web Framework' +thisyear = datetime.datetime.now().year +copyright = '2008-%s, Agendaless Consulting' % thisyear # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. -version = '1.1a0' +version = pkg_resources.get_distribution('pyramid').version + # The full version, including alpha/beta/rc tags. release = version @@ -103,68 +111,53 @@ release = version # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directories, that shouldn't be searched -# for source files. -#exclude_dirs = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_themes/README.rst', ] # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. #pygments_style = book and 'bw' or 'tango' if book: pygments_style = 'bw' -# The default language to highlight source code in. -#highlight_language = 'guess' - # Options for HTML output # ----------------------- +# enable pylons_sphinx_latesturl when this branch is no longer "latest" +# pylons_sphinx_latesturl_base = ( +# 'http://docs.pylonsproject.org/projects/pyramid/en/latest/') +# pylons_sphinx_latesturl_pagename_overrides = { +# # map old pagename -> new pagename +# 'whatsnew-1.0': 'index', +# 'whatsnew-1.1': 'index', +# 'whatsnew-1.2': 'index', +# 'whatsnew-1.3': 'index', +# 'whatsnew-1.4': 'index', +# 'whatsnew-1.5': 'index', +# 'whatsnew-1.6': 'index', +# 'whatsnew-1.7': 'index', +# 'tutorials/gae/index': 'index', +# 'api/chameleon_text': 'api', +# 'api/chameleon_zpt': 'api', +# } -# Add and use Pylons theme -sys.path.append(os.path.abspath('_themes')) -html_theme_path = ['_themes'] html_theme = 'pyramid' - -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -#html_style = 'pyramid.css' +html_theme_path = pylons_sphinx_themes.get_html_themes_path() +html_theme_options = dict( + github_url='https://github.com/Pylons/pyramid', + # On master branch and new branch still in + # pre-release status: true; else: false. + in_progress='true', + # On branches previous to "latest": true; else: false. + outdated='false', + ) # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". -html_title = 'The Pyramid Web Application Development Framework v%s' % release - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = 'Home' - -# The name of an image file (within the static path) to place at the top of -# the sidebar. -#html_logo = '_static/pyramid.png' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = '_static/pyramid.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +html_title = 'The Pyramid Web Framework v%s' % release # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -172,34 +165,7 @@ html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, the reST sources are included in the HTML build as _sources/<name>. -#html_copy_source = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +html_use_smartypants = False # people use cutnpaste in some places # Output file base name for HTML help builder. htmlhelp_basename = 'pyramid' @@ -213,28 +179,20 @@ latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). latex_font_size = '10pt' +latex_additional_files = ['_static/latex-note.png', '_static/latex-warning.png'] + # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('latexindex', 'pyramid.tex', - 'The Pyramid Web Application Development Framework', + 'The Pyramid Web Framework', 'Chris McDonough', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = '_static/pylons_small.png' - # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. latex_use_parts = True -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - # If false, no module index is generated. latex_use_modindex = False @@ -349,10 +307,10 @@ _PREAMBLE = r""" latex_elements = { 'preamble': _PREAMBLE, - 'wrapperclass':'book', - 'date':'', - 'releasename':'Version', - 'title':r'The Pyramid Web Application \newline Development Framework', + 'wrapperclass': 'book', + 'date': '', + 'releasename': 'Version', + 'title': r'The Pyramid Web Framework', # 'pointsize':'12pt', # uncomment for 12pt version } @@ -368,6 +326,7 @@ latex_elements = { #paragraph 4 #subparagraph 5 + def frontmatter(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): return [nodes.raw( @@ -381,10 +340,11 @@ def frontmatter(name, arguments, options, content, lineno, % reset page counter \setcounter{page}{1} % suppress first toc pagenum -\addtocontents{toc}{\protect\thispagestyle{empty}} +\addtocontents{toc}{\protect\thispagestyle{empty}} """, format='latex')] + def mainmatter(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): return [nodes.raw( @@ -394,7 +354,7 @@ def mainmatter(name, arguments, options, content, lineno, % allow part/chapter/section numbering \setcounter{secnumdepth}{2} % get headers back -\pagestyle{fancy} +\pagestyle{fancy} \fancyhf{} \renewcommand{\headrulewidth}{0.5pt} \renewcommand{\footrulewidth}{0pt} @@ -404,11 +364,13 @@ def mainmatter(name, arguments, options, content, lineno, """, format='latex')] + def backmatter(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): return [nodes.raw('', '\\backmatter\n\\setcounter{secnumdepth}{-1}\n', format='latex')] + def app_role(role, rawtext, text, lineno, inliner, options={}, content=[]): """custom role for :app: marker, does nothing in particular except allow :app:`Pyramid` to work (for later search and replace).""" @@ -426,6 +388,7 @@ def setup(app): app.add_directive('backmatter', backmatter, 1, (0, 0, 0)) app.connect('autodoc-process-signature', resig) + def resig(app, what, name, obj, options, signature, return_annotation): """ Allow for preservation of ``@action_method`` decorated methods in configurator """ @@ -452,10 +415,11 @@ def resig(app, what, name, obj, options, signature, return_annotation): # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = 'The Pyramid Web Application Development Framework, Version 1.0' +epub_title = 'The Pyramid Web Framework, Version %s' \ + % release epub_author = 'Chris McDonough' epub_publisher = 'Agendaless Consulting' -epub_copyright = '2008-2011' +epub_copyright = '2008-%d' % thisyear # The language of the text. It defaults to the language option # or en if the language is not set. @@ -469,18 +433,16 @@ epub_scheme = 'ISBN' epub_identifier = '0615445675' # A unique identification for the text. -epub_uid = 'The Pyramid Web Application Development Framework, Version 1.0' - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] +epub_uid = 'The Pyramid Web Framework, Version %s' \ + % release # A list of files that should not be packed into the epub file. -#epub_exclude_files = [] +epub_exclude_files = ['_static/opensearch.xml', '_static/doctools.js', + '_static/jquery.js', '_static/searchtools.js', '_static/underscore.js', + '_static/basic.css', 'search.html', '_static/websupport.js'] + # The depth of the table of contents in toc.ncx. epub_tocdepth = 3 + +# For a list of all settings, visit http://sphinx-doc.org/config.html diff --git a/docs/conventions.rst b/docs/conventions.rst index 9e8510e4d..4469d0c73 100644 --- a/docs/conventions.rst +++ b/docs/conventions.rst @@ -1,19 +1,19 @@ Typographical Conventions ========================= -Literals, filenames and function arguments are presented using the +Literals, filenames, and function arguments are presented using the following style: ``argument1`` -Warnings, which represent limitations and need-to-know information +Warnings which represent limitations and need-to-know information related to a topic or concept are presented in the following style: .. warning:: This is a warning. -Notes, which represent additional information related to a topic or +Notes which represent additional information related to a topic or concept are presented in the following style: .. note:: @@ -24,7 +24,7 @@ We present Python method names using the following style: :meth:`pyramid.config.Configurator.add_view` -We present Python class names, module names, attributes and global +We present Python class names, module names, attributes, and global variables using the following style: :class:`pyramid.config.Configurator.registry` @@ -50,19 +50,65 @@ Code and configuration file blocks are presented in the following style: def foo(abc): pass -When a command that should be typed on one line is too long to fit on -a page, the backslash ``\`` is used to indicate that the following -printed line should actually be part of the command: +Example blocks representing UNIX shell commands are prefixed with a ``$`` +character, e.g.: - .. code-block:: text + .. code-block:: bash - c:\bigfntut\tutorial> ..\Scripts\nosetests --cover-package=tutorial \ - --cover-erase --with-coverage + $ $VENV/bin/py.test tutorial/tests.py -q -A sidebar, which presents a concept tangentially related to content -discussed on a page, is rendered like so: +(See :term:`venv` for the meaning of ``$VENV``) + +Example blocks representing Windows ``cmd.exe`` commands are prefixed with a +drive letter and/or a directory name, e.g.: + + .. code-block:: doscon + + c:\examples> %VENV%\Scripts\py.test tutorial\tests.py -q + +(See :term:`venv` for the meaning of ``%VENV%``) + +Sometimes, when it's unknown which directory is current, Windows ``cmd.exe`` +example block commands are prefixed only with a ``>`` character, e.g.: + + .. code-block:: doscon + + > %VENV%\Scripts\py.test tutorial\tests.py -q + +When a command that should be typed on one line is too long to fit on a page, +the backslash ``\`` is used to indicate that the following printed line should +be part of the command: + + .. code-block:: bash + + $VENV/bin/py.test tutorial/tests.py --cov-report term-missing \ + --cov=tutorial -q + +A sidebar, which presents a concept tangentially related to content discussed +on a page, is rendered like so: .. sidebar:: This is a sidebar Sidebar information. +When multiple objects are imported from the same package, the following +convention is used: + + .. code-block:: python + + from foo import ( + bar, + baz, + ) + +It may look unusual, but it has advantages: + +* It allows one to swap out the higher-level package ``foo`` for something else + that provides the similar API. An example would be swapping out one database + for another (e.g., graduating from SQLite to PostgreSQL). + +* Looks more neat in cases where a large number of objects get imported from + that package. + +* Adding or removing imported objects from the package is quicker and results + in simpler diffs. diff --git a/docs/copyright.rst b/docs/copyright.rst index 1c7aa5844..3beaee7f7 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -1,7 +1,7 @@ Copyright, Trademarks, and Attributions ======================================= -*The Pyramid Web Application Development Framework, Version 1.0* +*The Pyramid Web Framework, Version 1.1* by Chris McDonough @@ -39,7 +39,7 @@ any trademark or service mark. Every effort has been made to make this book as complete and as accurate as possible, but no warranty or fitness is implied. The -information provided is on as "as-is" basis. The author and the +information provided is on an "as-is" basis. The author and the publisher shall have neither liability nor responsibility to any person or entity with respect to any loss or damages arising from the information contained in this book. No patent liability is assumed @@ -55,7 +55,12 @@ Contributors: Ben Bangert, Blaise Laflamme, Rob Miller, Mike Orr, Carlos de la Guardia, Paul Everitt, Tres Seaver, John Shipman, Marius Gedminas, Chris Rossi, Joachim Krebs, Xavier Spriet, Reed O'Brien, William Chambers, Charlie - Choiniere, Jamaludin Ahmad, Graham Higgins, Patricio Paez. + Choiniere, Jamaludin Ahmad, Graham Higgins, Patricio Paez, Michael + Merickel, Eric Ongerth, Niall O'Higgins, Christoph Zwerschke, John + Anderson, Atsushi Odagiri, Kirk Strauser, JD Navarro, Joe Dallago, + Savoir-Faire Linux, Łukasz Fidosz, Christopher Lambacher, Claus Conrad, + Chris Beelby, Phil Jenvey and a number of people with only pseudonyms on + GitHub. Cover Designer: Hugues Laflamme of `Kemeneur <http://www.kemeneur.com/>`_. @@ -68,6 +73,9 @@ Used with permission: The :ref:`much_ado_about_traversal_chapter` chapter is adapted, with permission, from an article written by Rob Miller. + The :ref:`logging_chapter` is adapted, with permission, from the Pylons + documentation logging chapter, originally written by Phil Jenvey. + Print Production ---------------- @@ -81,14 +89,14 @@ Contacting The Publisher Please send documentation licensing inquiries, translation inquiries, and other business communications to `Agendaless Consulting <mailto:webmaster@agendaless.com>`_. Please send software and other -technical queries to the `Pylons-devel maillist +technical queries to the `Pylons-devel mailing list <http://groups.google.com/group/pylons-devel>`_. HTML Version and Source Code ---------------------------- An HTML version of this book is freely available via -http://docs.pylonsproject.org +http://docs.pylonsproject.org/projects/pyramid/en/latest/ The source code for the examples used in this book are available within the :app:`Pyramid` software distribution, always available diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 0321113fa..5f3295305 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -7,98 +7,94 @@ From time to time, challenges to various aspects of :app:`Pyramid` design are lodged. To give context to discussions that follow, we detail some of the design decisions and trade-offs here. In some cases, we acknowledge that the framework can be made better and we describe future steps which will be taken -to improve it; in some cases we just file the challenge as "noted", as -obviously you can't please everyone all of the time. +to improve it. In others we just file the challenge as noted, as obviously you +can't please everyone all of the time. Pyramid Provides More Than One Way to Do It ------------------------------------------- A canon of Python popular culture is "TIOOWTDI" ("there is only one way to do -it", a slighting, tongue-in-cheek reference to Perl's "TIMTOWTDI", which is -an acronym for "there is more than one way to do it"). - -:app:`Pyramid` is, for better or worse, a "TIMTOWTDI" system. For example, -it includes more than one way to resolve a URL to a :term:`view callable`: -via :term:`url dispatch` or :term:`traversal`. Multiple methods of -configuration exist: :term:`imperative configuration`, :term:`configuration -decoration`, and :term:`ZCML` (optionally via :term:`pyramid_zcml`). It works -with multiple different kinds of persistence and templating systems. And so -on. However, the existence of most of these overlapping ways to do things -are not without reason and purpose: we have a number of audiences to serve, -and we believe that TIMTOWTI at the web framework level actually *prevents* a -much more insidious and harmful set of duplication at higher levels in the -Python web community. - -:app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of -people with many years of prior :term:`Zope` experience. The idea of +it", a slighting, tongue-in-cheek reference to Perl's "TIMTOWTDI", which is an +acronym for "there is more than one way to do it"). + +:app:`Pyramid` is, for better or worse, a "TIMTOWTDI" system. For example, it +includes more than one way to resolve a URL to a :term:`view callable`: via +:term:`url dispatch` or :term:`traversal`. Multiple methods of configuration +exist: :term:`imperative configuration`, :term:`configuration decoration`, and +:term:`ZCML` (optionally via :term:`pyramid_zcml`). It works with multiple +different kinds of persistence and templating systems. And so on. However, the +existence of most of these overlapping ways to do things are not without reason +and purpose: we have a number of audiences to serve, and we believe that +TIMTOWTDI at the web framework level actually *prevents* a much more insidious +and harmful set of duplication at higher levels in the Python web community. + +:app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of people +with many years of prior :term:`Zope` experience. The idea of :term:`traversal` and the way :term:`view lookup` works was stolen entirely from Zope. The authorization subsystem provided by :app:`Pyramid` is a derivative of Zope's. The idea that an application can be *extended* without forking is also a Zope derivative. Implementations of these features were *required* to allow the :app:`Pyramid` -authors to build the bread-and-butter CMS-type systems for customers in the -way they were accustomed to building them. No other system save Zope itself -had such features. And Zope itself was beginning to show signs of its age. -We were becoming hampered by consequences of its early design mistakes. -Zope's lack of documentation was also difficult to work around: it was hard -to hire smart people to work on Zope applications, because there was no -comprehensive documentation set to point them at which explained "it all" in -one consumble place, and it was too large and self-inconsistent to document -properly. Before :mod:`repoze.bfg` went under development, its authors -obviously looked around for other frameworks that fit the bill. But no -non-Zope framework did. So we embarked on building :mod:`repoze.bfg`. +authors to build the bread-and-butter CMS-type systems for customers in the way +in which they were accustomed. No other system, save for Zope itself, had such +features, and Zope itself was beginning to show signs of its age. We were +becoming hampered by consequences of its early design mistakes. Zope's lack of +documentation was also difficult to work around. It was hard to hire smart +people to work on Zope applications because there was no comprehensive +documentation set which explained "it all" in one consumable place, and it was +too large and self-inconsistent to document properly. Before :mod:`repoze.bfg` +went under development, its authors obviously looked around for other +frameworks that fit the bill. But no non-Zope framework did. So we embarked on +building :mod:`repoze.bfg`. As the result of our research, however, it became apparent that, despite the -fact that no *one* framework had all the features we required, lots of -existing frameworks had good, and sometimes very compelling ideas. In -particular, :term:`URL dispatch` is a more direct mechanism to map URLs to -code. +fact that no *one* framework had all the features we required, lots of existing +frameworks had good, and sometimes very compelling ideas. In particular, +:term:`URL dispatch` is a more direct mechanism to map URLs to code. -So although we couldn't find a framework save for Zope that fit our needs, +So, although we couldn't find a framework, save for Zope, that fit our needs, and while we incorporated a lot of Zope ideas into BFG, we also emulated the features we found compelling in other frameworks (such as :term:`url -dispatch`). After the initial public release of BFG, as time went on, -features were added to support people allergic to various Zope-isms in the -system, such as the ability to configure the application using -:term:`imperative configuration` rather than solely using :term:`ZCML`, and -the elimination of the required use of :term:`interface` objects. It soon -became clear that we had a system that was very generic, and was beginning to -appeal to non-Zope users as well as ex-Zope users. +dispatch`). After the initial public release of BFG, as time went on, features +were added to support people allergic to various Zope-isms in the system, such +as the ability to configure the application using :term:`imperative +configuration` and :term:`configuration decoration`, rather than solely using +:term:`ZCML`, and the elimination of the required use of :term:`interface` +objects. It soon became clear that we had a system that was very generic, and +was beginning to appeal to non-Zope users as well as ex-Zope users. As the result of this generalization, it became obvious BFG shared 90% of its -featureset with the featureset of Pylons 1, and thus had a very similar -target market. Because they were so similar, choosing between the two -systems was an exercise in frustration for an otherwise non-partisan -developer. It was also strange for the Pylons and BFG development -communities to be in competition for the same set of users, given how similar -the two frameworks were. So the Pylons and BFG teams began to work together -to form a plan to "merge". The features missing from BFG (notably -:term:`view handler` classes, flash messaging, and other minor missing bits), -were added, to provide familiarity to ex-Pylons users. The result is -:app:`Pyramid`. - -The Python web framework space is currently notoriously balkanized. We're -truly hoping that the amalgamation of components in :app:`Pyramid` will -appeal to at least two currently very distinct sets of users: Pylons and BFG -users. By unifying the best concepts from Pylons and BFG into a single -codebase and leaving the bad concepts from their ancestors behind, we'll be -able to consolidate our efforts better, share more code, and promote our -efforts as a unit rather than competing pointlessly. We hope to be able to -shortcut the pack mentality which results in a *much larger* duplication of -effort, represented by competing but incredibly similar applications and -libraries, each built upon a specific low level stack that is incompatible -with the other. We'll also shrink the choice of credible Python web -frameworks down by at least one. We're also hoping to attract users from -other communities (such as Zope's and TurboGears') by providing the features -they require, while allowing enough flexibility to do things in a familiar -fashion. Some overlap of functionality to achieve these goals is expected -and unavoidable, at least if we aim to prevent pointless duplication at -higher levels. If we've done our job well enough, the various audiences will -be able to coexist and cooperate rather than firing at each other across some -imaginary web framework "DMZ". - -Pyramid Uses A Zope Component Architecture ("ZCA") Registry +feature set with the feature set of Pylons 1, and thus had a very similar +target market. Because they were so similar, choosing between the two systems +was an exercise in frustration for an otherwise non-partisan developer. It was +also strange for the Pylons and BFG development communities to be in +competition for the same set of users, given how similar the two frameworks +were. So the Pylons and BFG teams began to work together to form a plan to +merge. The features missing from BFG (notably :term:`view handler` classes, +flash messaging, and other minor missing bits), were added to provide +familiarity to ex-Pylons users. The result is :app:`Pyramid`. + +The Python web framework space is currently notoriously balkanized. We're truly +hoping that the amalgamation of components in :app:`Pyramid` will appeal to at +least two currently very distinct sets of users: Pylons and BFG users. By +unifying the best concepts from Pylons and BFG into a single codebase, and +leaving the bad concepts from their ancestors behind, we'll be able to +consolidate our efforts better, share more code, and promote our efforts as a +unit rather than competing pointlessly. We hope to be able to shortcut the pack +mentality which results in a *much larger* duplication of effort, represented +by competing but incredibly similar applications and libraries, each built upon +a specific low level stack that is incompatible with the other. We'll also +shrink the choice of credible Python web frameworks down by at least one. We're +also hoping to attract users from other communities (such as Zope's and +TurboGears') by providing the features they require, while allowing enough +flexibility to do things in a familiar fashion. Some overlap of functionality +to achieve these goals is expected and unavoidable, at least if we aim to +prevent pointless duplication at higher levels. If we've done our job well +enough, the various audiences will be able to coexist and cooperate rather than +firing at each other across some imaginary web framework DMZ. + +Pyramid Uses a Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- :app:`Pyramid` uses a :term:`Zope Component Architecture` (ZCA) "component @@ -113,14 +109,13 @@ involves. Problems ++++++++ -The "global" API that may be used to access data in a ZCA "component -registry" is not particularly pretty or intuitive, and sometimes it's just -plain obtuse. Likewise, the conceptual load on a casual source code reader -of code that uses the ZCA global API is somewhat high. Consider a ZCA -neophyte reading the code that performs a typical "unnamed utility" lookup -using the :func:`zope.component.getUtility` global API: +The global API that may be used to access data in a ZCA component registry +is not particularly pretty or intuitive, and sometimes it's just plain +obtuse. Likewise, the conceptual load on a casual source code reader of code +that uses the ZCA global API is somewhat high. Consider a ZCA neophyte +reading the code that performs a typical "unnamed utility" lookup using the +:func:`zope.component.getUtility` global API: -.. ignore-next-block .. code-block:: python :linenos: @@ -129,7 +124,7 @@ using the :func:`zope.component.getUtility` global API: settings = getUtility(ISettings) After this code runs, ``settings`` will be a Python dictionary. But it's -unlikely that any "civilian" would know that just by reading the code. There +unlikely that any civilian would know that just by reading the code. There are a number of comprehension issues with the bit of code above that are obvious. @@ -141,13 +136,13 @@ of such code need to understand the concept in order to parse it. This is problem number one. Second, what's this ``ISettings`` thing? It's an :term:`interface`. Is that -important here? Not really, we're just using it as a "key" for some lookup +important here? Not really, we're just using it as a key for some lookup based on its identity as a marker: it represents an object that has the dictionary API, but that's not very important in this context. That's problem number two. Third of all, what does the ``getUtility`` function do? It's performing a -lookup for the ``ISettings`` "utility" that should return.. well, a utility. +lookup for the ``ISettings`` "utility" that should return... well, a utility. Note how we've already built up a dependency on the understanding of an :term:`interface` and the concept of "utility" to answer this question: a bad sign so far. Note also that the answer is circular, a *really* bad sign. @@ -157,50 +152,48 @@ registry" of course. What's a component registry? Problem number four. Fifth, assuming you buy that there's some magical registry hanging around, where *is* this registry? *Homina homina*... "around"? That's sort of the -best answer in this context (a more specific answer would require knowledge -of internals). Can there be more than one registry? Yes. So *which* -registry does it find the registration in? Well, the "current" registry of -course. In terms of :app:`Pyramid`, the current registry is a thread local -variable. Using an API that consults a thread local makes understanding how -it works non-local. - -You've now bought in to the fact that there's a registry that is just -"hanging around". But how does the registry get populated? Why, via code -that calls directives like ``config.add_view``. In this particular case, -however, the registration of ``ISettings`` is made by the framework itself -"under the hood": it's not present in any user configuration. This is -extremely hard to comprehend. Problem number six. - -Clearly there's some amount of cognitive load here that needs to be borne by -a reader of code that extends the :app:`Pyramid` framework due to its use of -the ZCA, even if he or she is already an expert Python programmer and whom is -an expert in the domain of web applications. This is suboptimal. +best answer in this context (a more specific answer would require knowledge of +internals). Can there be more than one registry? Yes. So in *which* registry +does it find the registration? Well, the "current" registry of course. In +terms of :app:`Pyramid`, the current registry is a thread local variable. +Using an API that consults a thread local makes understanding how it works +non-local. + +You've now bought in to the fact that there's a registry that is just hanging +around. But how does the registry get populated? Why, via code that calls +directives like ``config.add_view``. In this particular case, however, the +registration of ``ISettings`` is made by the framework itself under the hood: +it's not present in any user configuration. This is extremely hard to +comprehend. Problem number six. + +Clearly there's some amount of cognitive load here that needs to be borne by a +reader of code that extends the :app:`Pyramid` framework due to its use of the +ZCA, even if they are already an expert Python programmer and an expert in the +domain of web applications. This is suboptimal. Ameliorations +++++++++++++ First, the primary amelioration: :app:`Pyramid` *does not expect application -developers to understand ZCA concepts or any of its APIs*. If an -*application* developer needs to understand a ZCA concept or API during the -creation of a :app:`Pyramid` application, we've failed on some axis. +developers to understand ZCA concepts or any of its APIs*. If an *application* +developer needs to understand a ZCA concept or API during the creation of a +:app:`Pyramid` application, we've failed on some axis. -Instead, the framework hides the presence of the ZCA registry behind +Instead the framework hides the presence of the ZCA registry behind special-purpose API functions that *do* use ZCA APIs. Take for example the ``pyramid.security.authenticated_userid`` function, which returns the userid present in the current request or ``None`` if no userid is present in the current request. The application developer calls it like so: -.. ignore-next-block .. code-block:: python :linenos: from pyramid.security import authenticated_userid userid = authenticated_userid(request) -He now has the current user id. +They now have the current user id. -Under its hood however, the implementation of ``authenticated_userid`` -is this: +Under its hood however, the implementation of ``authenticated_userid`` is this: .. code-block:: python :linenos: @@ -217,56 +210,56 @@ is this: return policy.authenticated_userid(request) Using such wrappers, we strive to always hide the ZCA API from application -developers. Application developers should just never know about the ZCA API: -they should call a Python function with some object germane to the domain as -an argument, and it should returns a result. A corollary that follows is -that any reader of an application that has been written using :app:`Pyramid` -needn't understand the ZCA API either. +developers. Application developers should just never know about the ZCA API; +they should call a Python function with some object germane to the domain as an +argument, and it should return a result. A corollary that follows is that any +reader of an application that has been written using :app:`Pyramid` needn't +understand the ZCA API either. Hiding the ZCA API from application developers and code readers is a form of -enhancing "domain specificity". No application developer wants to need to -understand the small, detailed mechanics of how a web framework does its -thing. People want to deal in concepts that are closer to the domain they're -working in: for example, web developers want to know about *users*, not -*utilities*. :app:`Pyramid` uses the ZCA as an implementation detail, not as -a feature which is exposed to end users. +enhancing domain specificity. No application developer wants to need to +understand the small, detailed mechanics of how a web framework does its thing. +People want to deal in concepts that are closer to the domain they're working +in. For example, web developers want to know about *users*, not *utilities*. +:app:`Pyramid` uses the ZCA as an implementation detail, not as a feature which +is exposed to end users. However, unlike application developers, *framework developers*, including people who want to override :app:`Pyramid` functionality via preordained -framework plugpoints like traversal or view lookup *must* understand the ZCA +framework plugpoints like traversal or view lookup, *must* understand the ZCA registry API. :app:`Pyramid` framework developers were so concerned about conceptual load -issues of the ZCA registry API for framework developers that a `replacement -registry implementation <http://svn.repoze.org/repoze.component/trunk>`_ -named :mod:`repoze.component` was actually developed. Though this package -has a registry implementation which is fully functional and well-tested, and -its API is much nicer than the ZCA registry API, work on it was largely -abandoned and it is not used in :app:`Pyramid`. We continued to use a ZCA -registry within :app:`Pyramid` because it ultimately proved a better fit. - -.. note:: We continued using ZCA registry rather than disusing it in - favor of using the registry implementation in - :mod:`repoze.component` largely because the ZCA concept of - interfaces provides for use of an interface hierarchy, which is - useful in a lot of scenarios (such as context type inheritance). - Coming up with a marker type that was something like an interface - that allowed for this functionality seemed like it was just - reinventing the wheel. - -Making framework developers and extenders understand the ZCA registry API is -a trade-off. We (the :app:`Pyramid` developers) like the features that the -ZCA registry gives us, and we have long-ago borne the weight of understanding -what it does and how it works. The authors of :app:`Pyramid` understand the -ZCA deeply and can read code that uses it as easily as any other code. +issues of the ZCA registry API that a `replacement registry implementation +<https://github.com/repoze/repoze.component>`_ named :mod:`repoze.component` +was actually developed. Though this package has a registry implementation +which is fully functional and well-tested, and its API is much nicer than the +ZCA registry API, work on it was largely abandoned, and it is not used in +:app:`Pyramid`. We continued to use a ZCA registry within :app:`Pyramid` +because it ultimately proved a better fit. + +.. note:: + + We continued using ZCA registry rather than disusing it in favor of using + the registry implementation in :mod:`repoze.component` largely because the + ZCA concept of interfaces provides for use of an interface hierarchy, which + is useful in a lot of scenarios (such as context type inheritance). Coming + up with a marker type that was something like an interface that allowed for + this functionality seemed like it was just reinventing the wheel. + +Making framework developers and extenders understand the ZCA registry API is a +trade-off. We (the :app:`Pyramid` developers) like the features that the ZCA +registry gives us, and we have long-ago borne the weight of understanding what +it does and how it works. The authors of :app:`Pyramid` understand the ZCA +deeply and can read code that uses it as easily as any other code. But we recognize that developers who might want to extend the framework are not -as comfortable with the ZCA registry API as the original developers are with -it. So, for the purposes of being kind to third-party :app:`Pyramid` -framework developers in, we've drawn some lines in the sand. +as comfortable with the ZCA registry API as the original developers. So for +the purpose of being kind to third-party :app:`Pyramid` framework developers, +we've drawn some lines in the sand. -In all "core" code, We've made use of ZCA global API functions such as -``zope.component.getUtility`` and ``zope.component.getAdapter`` the exception +In all core code, we've made use of ZCA global API functions, such as +``zope.component.getUtility`` and ``zope.component.getAdapter``, the exception instead of the rule. So instead of: .. code-block:: python @@ -286,9 +279,9 @@ instead of the rule. So instead of: registry = get_current_registry() policy = registry.getUtility(IAuthenticationPolicy) -While the latter is more verbose, it also arguably makes it more obvious -what's going on. All of the :app:`Pyramid` core code uses this pattern -rather than the ZCA global API. +While the latter is more verbose, it also arguably makes it more obvious what's +going on. All of the :app:`Pyramid` core code uses this pattern rather than +the ZCA global API. Rationale +++++++++ @@ -307,40 +300,38 @@ the ZCA registry: mapping is done. - Features. The ZCA component registry essentially provides what can be - considered something like a "superdictionary", which allows for more - complex lookups than retrieving a value based on a single key. Some of - this lookup capability is very useful for end users, such as being able to - register a view that is only found when the context is some class of - object, or when the context implements some :term:`interface`. - -- Singularity. There's only one "place" where "application configuration" - lives in a :app:`Pyramid` application: in a component registry. The - component registry answers questions made to it by the framework at runtime - based on the configuration of *an application*. Note: "an application" is - not the same as "a process", multiple independently configured copies of - the same :app:`Pyramid` application are capable of running in the same - process space. + considered something like a superdictionary, which allows for more complex + lookups than retrieving a value based on a single key. Some of this lookup + capability is very useful for end users, such as being able to register a + view that is only found when the context is some class of object, or when + the context implements some :term:`interface`. + +- Singularity. There's only one place where "application configuration" lives + in a :app:`Pyramid` application: in a component registry. The component + registry answers questions made to it by the framework at runtime based on + the configuration of *an application*. Note: "an application" is not the + same as "a process"; multiple independently configured copies of the same + :app:`Pyramid` application are capable of running in the same process space. - Composability. A ZCA component registry can be populated imperatively, or there's an existing mechanism to populate a registry via the use of a - configuration file (ZCML, via :term:`pyramid_zcml`). We didn't need to - write a frontend from scratch to make use of configuration-file-driven - registry population. + configuration file (ZCML, via the optional :term:`pyramid_zcml` package). + We didn't need to write a frontend from scratch to make use of + configuration-file-driven registry population. - Pluggability. Use of the ZCA registry allows for framework extensibility via a well-defined and widely understood plugin architecture. As long as framework developers and extenders understand the ZCA registry, it's possible to extend :app:`Pyramid` almost arbitrarily. For example, it's - relatively easy to build a directive that registers several views "all at - once", allowing app developers to use that directive as a "macro" in code + relatively easy to build a directive that registers several views all at + once, allowing app developers to use that directive as a "macro" in code that they write. This is somewhat of a differentiating feature from other (non-Zope) frameworks. - Testability. Judicious use of the ZCA registry in framework code makes - testing that code slightly easier. Instead of using monkeypatching or - other facilities to register mock objects for testing, we inject - dependencies via ZCA registrations and then use lookups in the code find - our mock objects. + testing that code slightly easier. Instead of using monkeypatching or other + facilities to register mock objects for testing, we inject dependencies via + ZCA registrations, then use lookups in the code to find our mock objects. - Speed. The ZCA registry is very fast for a specific set of complex lookup scenarios that :app:`Pyramid` uses, having been optimized through the years @@ -354,91 +345,25 @@ Conclusion ++++++++++ If you only *develop applications* using :app:`Pyramid`, there's not much to -complain about here. You just should never need to understand the ZCA -registry API: use documented :app:`Pyramid` APIs instead. However, you may -be an application developer who doesn't read API documentation because it's -unmanly. Instead you read the raw source code, and because you haven't read -the documentation, you don't know what functions, classes, and methods even -*form* the :app:`Pyramid` API. As a result, you've now written code that -uses internals and you've painted yourself into a conceptual corner as a -result of needing to wrestle with some ZCA-using implementation detail. If -this is you, it's extremely hard to have a lot of sympathy for you. You'll -either need to get familiar with how we're using the ZCA registry or you'll -need to use only the documented APIs; that's why we document them as APIs. +complain about here. You just should never need to understand the ZCA registry +API; use documented :app:`Pyramid` APIs instead. However, you may be an +application developer who doesn't read API documentation. Instead you +read the raw source code, and because you haven't read the API documentation, +you don't know what functions, classes, and methods even *form* the +:app:`Pyramid` API. As a result, you've now written code that uses internals, +and you've painted yourself into a conceptual corner, needing to wrestle with +some ZCA-using implementation detail. If this is you, it's extremely hard to +have a lot of sympathy for you. You'll either need to get familiar with how +we're using the ZCA registry or you'll need to use only the documented APIs; +that's why we document them as APIs. If you *extend* or *develop* :app:`Pyramid` (create new directives, use some -of the more obscure "hooks" as described in :ref:`hooks_chapter`, or work on +of the more obscure hooks as described in :ref:`hooks_chapter`, or work on the :app:`Pyramid` core code), you will be faced with needing to understand at least some ZCA concepts. In some places it's used unabashedly, and will be forever. We know it's quirky, but it's also useful and fundamentally understandable if you take the time to do some reading about it. -Pyramid Uses Interfaces Too Liberally -------------------------------------- - -In this `TOPP Engineering blog entry -<http://www.coactivate.org/projects/topp-engineering/blog/2008/10/20/what-bothers-me-about-the-component-architecture/>`_, -Ian Bicking asserts that the way :mod:`repoze.bfg` used a Zope interface to -represent an HTTP request method added too much indirection for not enough -gain. We agreed in general, and for this reason, :mod:`repoze.bfg` version -1.1 (and subsequent versions including :app:`Pyramid` 1.0+) added :term:`view -predicate` and :term:`route predicate` modifiers to view configuration. -Predicates are request-specific (or :term:`context` -specific) matching -narrowers which don't use interfaces. Instead, each predicate uses a -domain-specific string as a match value. - -For example, to write a view configuration which matches only requests with -the ``POST`` HTTP request method, you might write a ``@view_config`` -decorator which mentioned the ``request_method`` predicate: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - @view_config(name='post_view', request_method='POST', renderer='json') - def post_view(request): - return 'POSTed' - -You might further narrow the matching scenario by adding an ``accept`` -predicate that narrows matching to something that accepts a JSON response: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - @view_config(name='post_view', request_method='POST', - accept='application/json', renderer='json') - def post_view(request): - return 'POSTed' - -Such a view would only match when the request indicated that HTTP request -method was ``POST`` and that the remote user agent passed -``application/json`` (or, for that matter, ``application/*``) in its -``Accept`` request header. - -"Under the hood", these features make no use of interfaces. - -Many "prebaked" predicates exist. However, use of only "prebaked" predicates, -however, doesn't entirely meet Ian's criterion. He would like to be able to -match a request using a lambda or another function which interrogates the -request imperatively. In :mod:`repoze.bfg` version 1.2, we acommodate this by -allowing people to define "custom" view predicates: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - from webob import Response - - def subpath(context, request): - return request.subpath and request.subpath[0] == 'abc' - - @view_config(custom_predicates=(subpath,)) - def aview(request): - return Response('OK') - -The above view will only match when the first element of the request's -:term:`subpath` is ``abc``. .. _zcml_encouragement: @@ -455,34 +380,21 @@ completely optional. No ZCML is required at all to use :app:`Pyramid`, nor any other sort of frameworky declarative frontend to application configuration. -.. _model_traversal_confusion: -Pyramid Uses "Model" To Represent A Node In The Graph of Objects Traversed --------------------------------------------------------------------------- - -The ``repoze.bfg`` documentation used to refer to the graph being traversed -when :term:`traversal` is used as a "model graph". A terminology overlap -confused people who wrote applications that always use ORM packages such as -SQLAlchemy, which has a different notion of the definition of a "model". As -a result, in Pyramid 1.0a7, the tree of objects traversed is now renamed to -:term:`resource tree` and its components are now named :term:`resource` -objects. Associated APIs have been changed. This hopefully alleviates the -terminology confusion caused by overriding the term "model". - -Pyramid Does Traversal, And I Don't Like Traversal +Pyramid Does Traversal, and I Don't Like Traversal -------------------------------------------------- In :app:`Pyramid`, :term:`traversal` is the act of resolving a URL path to a -:term:`resource` object in a resource tree. Some people are uncomfortable -with this notion, and believe it is wrong. Thankfully, if you use -:app:`Pyramid`, and you don't want to model your application in terms of a -resource tree, you needn't use it at all. Instead, use :term:`URL dispatch` -to map URL paths to views. +:term:`resource` object in a resource tree. Some people are uncomfortable with +this notion, and believe it is wrong. Thankfully if you use :app:`Pyramid` and +you don't want to model your application in terms of a resource tree, you +needn't use it at all. Instead use :term:`URL dispatch` to map URL paths to +views. -The idea that some folks believe traversal is unilaterally "wrong" is +The idea that some folks believe traversal is unilaterally wrong is understandable. The people who believe it is wrong almost invariably have all of their data in a relational database. Relational databases aren't -naturally hierarchical, so "traversing" one like a tree is not possible. +naturally hierarchical, so traversing one like a tree is not possible. However, folks who deem traversal unilaterally wrong are neglecting to take into account that many persistence mechanisms *are* hierarchical. Examples @@ -500,19 +412,20 @@ hierarchical: sections within sections within sections, ad infinitum. If you want your URLs to indicate this structure, and the structure is indefinite (the number of nested sections can be "N" instead of some fixed number), a resource tree is an excellent way to model this, even if the backend is a -relational database. In this situation, the resource tree a just a site +relational database. In this situation, the resource tree is just a site structure. Traversal also offers better composability of applications than URL dispatch, because it doesn't rely on a fixed ordering of URL matching. You can compose a set of disparate functionality (and add to it later) around a mapping of -view to resource more predictably than trying to get "the right" ordering of +view to resource more predictably than trying to get the right ordering of URL pattern matching. But the point is ultimately moot. If you don't want to use traversal, you needn't. Use URL dispatch instead. -Pyramid Does URL Dispatch, And I Don't Like URL Dispatch + +Pyramid Does URL Dispatch, and I Don't Like URL Dispatch -------------------------------------------------------- In :app:`Pyramid`, :term:`url dispatch` is the act of resolving a URL path to @@ -534,41 +447,40 @@ I'll argue that URL dispatch is ultimately useful, even if you want to use traversal as well. You can actually *combine* URL dispatch and traversal in :app:`Pyramid` (see :ref:`hybrid_chapter`). One example of such a usage: if you want to emulate something like Zope 2's "Zope Management Interface" UI on -top of your object graph (or any administrative interface), you can register -a route like ``<route name="manage" pattern="manage/*traverse"/>`` and then -associate "management" views in your code by using the ``route_name`` -argument to a ``view`` configuration, e.g. ``<view view=".some.callable" -context=".some.Resource" route_name="manage"/>``. If you wire things up this -way someone then walks up to for example, ``/manage/ob1/ob2``, they might be +top of your object graph (or any administrative interface), you can register a +route like ``config.add_route('manage', '/manage/*traverse')`` and then +associate "management" views in your code by using the ``route_name`` argument +to a ``view`` configuration, e.g., ``config.add_view('.some.callable', +context=".some.Resource", route_name='manage')``. If you wire things up this +way, someone then walks up to, for example, ``/manage/ob1/ob2``, they might be presented with a management interface, but walking up to ``/ob1/ob2`` would -present them with the default object view. There are other tricks you can -pull in these hybrid configurations if you're clever (and maybe masochistic) -too. - -Also, if you are a URL dispatch hater, if you should ever be asked to write -an application that must use some legacy relational database structure, you -might find that using URL dispatch comes in handy for one-off associations -between views and URL paths. Sometimes it's just pointless to add a node to -the object graph that effectively represents the entry point for some bit of -code. You can just use a route and be done with it. If a route matches, a -view associated with the route will be called; if no route matches, -:app:`Pyramid` falls back to using traversal. +present them with the default object view. There are other tricks you can pull +in these hybrid configurations if you're clever (and maybe masochistic) too. + +Also, if you are a URL dispatch hater, if you should ever be asked to write an +application that must use some legacy relational database structure, you might +find that using URL dispatch comes in handy for one-off associations between +views and URL paths. Sometimes it's just pointless to add a node to the object +graph that effectively represents the entry point for some bit of code. You +can just use a route and be done with it. If a route matches, a view +associated with the route will be called. If no route matches, :app:`Pyramid` +falls back to using traversal. But the point is ultimately moot. If you use :app:`Pyramid`, and you really don't want to use URL dispatch, you needn't use it at all. Instead, use :term:`traversal` exclusively to map URL paths to views, just like you do in :term:`Zope`. + Pyramid Views Do Not Accept Arbitrary Keyword Arguments ------------------------------------------------------- Many web frameworks (Zope, TurboGears, Pylons 1.X, Django) allow for their variant of a :term:`view callable` to accept arbitrary keyword or positional -arguments, which are "filled in" using values present in the ``request.POST`` -or ``request.GET`` dictionaries or by values present in the "route match -dictionary". For example, a Django view will accept positional arguments -which match information in an associated "urlconf" such as -``r'^polls/(?P<poll_id>\d+)/$``: +arguments, which are filled in using values present in the ``request.POST``, +``request.GET``, or route match dictionaries. For example, a Django view will +accept positional arguments which match information in an associated "urlconf" +such as ``r'^polls/(?P<poll_id>\d+)/$``: .. code-block:: python :linenos: @@ -576,10 +488,9 @@ which match information in an associated "urlconf" such as def aview(request, poll_id): return HttpResponse(poll_id) -Zope, likewise allows you to add arbitrary keyword and positional -arguments to any method of a resource object found via traversal: +Zope likewise allows you to add arbitrary keyword and positional arguments to +any method of a resource object found via traversal: -.. ignore-next-block .. code-block:: python :linenos: @@ -595,13 +506,13 @@ match the names of the positional and keyword arguments in the request, and the method is called (if possible) with its argument list filled with values mentioned therein. TurboGears and Pylons 1.X operate similarly. -Out of the box, :app:`Pyramid` is configured to have none of these features. -By default, :mod:`pyramid` view callables always accept only ``request`` and -no other arguments. The rationale: this argument specification matching done -aggressively can be costly, and :app:`Pyramid` has performance as one of its -main goals, so we've decided to make people, by default, obtain information -by interrogating the request object within the view callable body instead of -providing magic to do unpacking into the view argument list. +Out of the box, :app:`Pyramid` is configured to have none of these features. By +default :app:`Pyramid` view callables always accept only ``request`` and no +other arguments. The rationale is, this argument specification matching when +done aggressively can be costly, and :app:`Pyramid` has performance as one of +its main goals. Therefore we've decided to make people, by default, obtain +information by interrogating the request object within the view callable body +instead of providing magic to do unpacking into the view argument list. However, as of :app:`Pyramid` 1.0a9, user code can influence the way view callables are expected to be called, making it possible to compose a system @@ -611,7 +522,7 @@ out of view callables which are called with arbitrary arguments. See Pyramid Provides Too Few "Rails" -------------------------------- -By design, :app:`Pyramid` is not a particularly "opinionated" web framework. +By design, :app:`Pyramid` is not a particularly opinionated web framework. It has a relatively parsimonious feature set. It contains no built in ORM nor any particular database bindings. It contains no form generation framework. It has no administrative web user interface. It has no built in @@ -619,14 +530,18 @@ text indexing. It does not dictate how you arrange your code. Such opinionated functionality exists in applications and frameworks built *on top* of :app:`Pyramid`. It's intended that higher-level systems emerge -built using :app:`Pyramid` as a base. See also :ref:`apps_are_extensible`. +built using :app:`Pyramid` as a base. + +.. seealso:: + + See also :ref:`apps_are_extensible`. Pyramid Provides Too Many "Rails" --------------------------------- :app:`Pyramid` provides some features that other web frameworks do not. These are features meant for use cases that might not make sense to you if -you're building a simple "bespoke" web application: +you're building a simple bespoke web application: - An optional way to map URLs to code using :term:`traversal` which implies a walk of a :term:`resource tree`. @@ -635,24 +550,24 @@ you're building a simple "bespoke" web application: sources using :meth:`pyramid.config.Configurator.include`. - View and subscriber registrations made using :term:`interface` objects - instead of class objects (e.g. :ref:`using_resource_interfaces`). + instead of class objects (e.g., :ref:`using_resource_interfaces`). - A declarative :term:`authorization` system. - Multiple separate I18N :term:`translation string` factories, each of which - can name its own "domain". + can name its own domain. These features are important to the authors of :app:`Pyramid`. The :app:`Pyramid` authors are often commissioned to build CMS-style -applications. Such applications are often "frameworky" because they have -more than one deployment. Each deployment requires a slightly different +applications. Such applications are often frameworky because they have more +than one deployment. Each deployment requires a slightly different composition of sub-applications, and the framework and sub-applications often need to be *extensible*. Because the application has more than one deployment, pluggability and extensibility is important, as maintaining multiple forks of the application, one per deployment, is extremely undesirable. Because it's easier to extend a system that uses -:term:`traversal` "from the outside" than it is to do the same in a system -that uses :term:`URL dispatch`, each deployment uses a :term:`resource tree` +:term:`traversal` from the outside than it is to do the same in a system that +uses :term:`URL dispatch`, each deployment uses a :term:`resource tree` composed of a persistent tree of domain model objects, and uses :term:`traversal` to map :term:`view callable` code to resources in the tree. The resource tree contains very granular security declarations, as resources @@ -661,90 +576,62 @@ make unit testing and implementation substitutability easier. In a bespoke web application, usually there's a single canonical deployment, and therefore no possibility of multiple code forks. Extensibility is not -required; the code is just changed in-place. Security requirements are often -less granular. Using the features listed above will often be overkill for -such an application. +required; the code is just changed in place. Security requirements are often +less granular. Using the features listed above will often be overkill for such +an application. If you don't like these features, it doesn't mean you can't or shouldn't use -:app:`Pyramid`. They are all optional, and a lot of time has been spent -making sure you don't need to know about them up-front. You can build -"Pylons-1.X-style" applications using :app:`Pyramid` that are purely bespoke -by ignoring the features above. You may find these features handy later -after building a "bespoke" web application that suddenly becomes popular and -requires extensibility because it must be deployed in multiple locations. +:app:`Pyramid`. They are all optional, and a lot of time has been spent making +sure you don't need to know about them up front. You can build "Pylons 1.X +style" applications using :app:`Pyramid` that are purely bespoke by ignoring +the features above. You may find these features handy later after building a +bespoke web application that suddenly becomes popular and requires +extensibility because it must be deployed in multiple locations. Pyramid Is Too Big ------------------ -"The :app:`Pyramid` compressed tarball is almost 2MB. It must be -enormous!" +"The :app:`Pyramid` compressed tarball is larger than 2MB. It must beenormous!" -No. We just ship it with test code and helper templates. Here's a -breakdown of what's included in subdirectories of the package tree: +No. We just ship it with docs, test code, and scaffolding. Here's a breakdown +of what's included in subdirectories of the package tree: docs/ - 3.0MB + 3.6MB pyramid/tests/ - 1.1MB + 1.3MB -pyramid/paster_templates/ +pyramid/scaffolds/ - 804KB + 133KB -pyramid/ (except for ``pyramd/tests and pyramid/paster_templates``) +pyramid/ (except for ``pyramd/tests`` and ``pyramid/scaffolds``) - 539K + 812KB -The actual :app:`Pyramid` runtime code is about 10% of the total size of the -tarball omitting docs, helper templates used for package generation, and test -code. Of the approximately 19K lines of Python code in the package, the code +Of the approximately 34K lines of Python code in the package, the code that actually has a chance of executing during normal operation, excluding -tests and paster template Python files, accounts for approximately 5K lines -of Python code. This is comparable to Pylons 1.X, which ships with a little -over 2K lines of Python code, excluding tests. +tests and scaffolding Python files, accounts for approximately 10K lines. + Pyramid Has Too Many Dependencies --------------------------------- -This is true. At the time of this writing, the total number of Python -package distributions that :app:`Pyramid` depends upon transitively is 18 if -you use Python 2.6 or 2.7, or 16 if you use Python 2.4 or 2.5. This is a lot -more than zero package distribution dependencies: a metric which various -Python microframeworks and Django boast. - -The :mod:`zope.component` and :mod:`zope.configuration` packages on which -:app:`Pyramid` depends have transitive dependencies on several other packages -(:mod:`zope.schema`, :mod:`zope.i18n`, :mod:`zope.event`, -:mod:`zope.interface`, :mod:`zope.deprecation`, :mod:`zope.i18nmessageid`). -We've been working with the Zope community to try to collapse and untangle -some of these dependencies. We'd prefer that these packages have fewer -packages as transitive dependencies, and that much of the functionality of -these packages was moved into a smaller *number* of packages. - -:app:`Pyramid` also has its own direct dependencies, such as :term:`Paste`, -:term:`Chameleon`, :term:`Mako` and :term:`WebOb`, and some of these in turn -have their own transitive dependencies. - -It should be noted that :app:`Pyramid` is positively lithe compared to -:term:`Grok`, a different Zope-based framework. As of this writing, in its -default configuration, Grok has 109 package distribution dependencies. The -number of dependencies required by :app:`Pyramid` is many times fewer than -Grok (or Zope itself, upon which Grok is based). :app:`Pyramid` has a number -of package distribution dependencies comparable to similarly-targeted -frameworks such as Pylons 1.X. - -We try not to reinvent too many wheels (at least the ones that don't need -reinventing), and this comes at the cost of some number of dependencies. -However, "number of package distributions" is just not a terribly great -metric to measure complexity. For example, the :mod:`zope.event` -distribution on which :app:`Pyramid` depends has a grand total of four lines -of runtime code. As noted above, we're continually trying to agitate for a -collapsing of these sorts of packages into fewer distribution files. - -Pyramid "Cheats" To Obtain Speed +Over time, we've made lots of progress on reducing the number of packaging +dependencies Pyramid has had. Pyramid 1.2 had 15 of them. Pyramid 1.3 and 1.4 +had 12 of them. The current release as of this writing, Pyramid 1.5, has +only 7. This number is unlikely to become any smaller. + +A port to Python 3 completed in Pyramid 1.3 helped us shed a good number of +dependencies by forcing us to make better packaging decisions. Removing +Chameleon and Mako templating system dependencies in the Pyramid core in 1.5 +let us shed most of the remainder of them. + + +Pyramid "Cheats" to Obtain Speed -------------------------------- Complaints have been lodged by other web framework authors at various times @@ -753,10 +640,11 @@ mechanism is our use (transitively) of the C extensions provided by :mod:`zope.interface` to do fast lookups. Another claimed cheating mechanism is the religious avoidance of extraneous function calls. -If there's such a thing as cheating to get better performance, we want to -cheat as much as possible. We optimize :app:`Pyramid` aggressively. This -comes at a cost: the core code has sections that could be expressed more -readably. As an amelioration, we've commented these sections liberally. +If there's such a thing as cheating to get better performance, we want to cheat +as much as possible. We optimize :app:`Pyramid` aggressively. This comes at a +cost. The core code has sections that could be expressed with more readability. +As an amelioration, we've commented these sections liberally. + Pyramid Gets Its Terminology Wrong ("MVC") ------------------------------------------ @@ -769,8 +657,8 @@ existing "MVC" framework uses its terminology. For example, you probably expect that models are ORM models, controllers are classes that have methods that map to URLs, and views are templates. :app:`Pyramid` indeed has each of these concepts, and each probably *works* almost exactly like your existing -"MVC" web framework. We just don't use the "MVC" terminology, as we can't -square its usage in the web framework space with historical reality. +"MVC" web framework. We just don't use the MVC terminology, as we can't square +its usage in the web framework space with historical reality. People very much want to give web applications the same properties as common desktop GUI platforms by using similar terminology, and to provide some frame @@ -779,198 +667,193 @@ hang together. But in the opinion of the author, "MVC" doesn't match the web very well in general. Quoting from the `Model-View-Controller Wikipedia entry <http://en.wikipedia.org/wiki/Model–view–controller>`_: -.. code-block:: text - - Though MVC comes in different flavors, control flow is generally as - follows: + Though MVC comes in different flavors, control flow is generally as + follows: - The user interacts with the user interface in some way (for - example, presses a mouse button). + The user interacts with the user interface in some way (for example, + presses a mouse button). - The controller handles the input event from the user interface, - often via a registered handler or callback and converts the event - into appropriate user action, understandable for the model. + The controller handles the input event from the user interface, often via + a registered handler or callback and converts the event into appropriate + user action, understandable for the model. - The controller notifies the model of the user action, possibly - resulting in a change in the model's state. (For example, the - controller updates the user's shopping cart.)[5] + The controller notifies the model of the user action, possibly resulting + in a change in the model's state. (For example, the controller updates the + user's shopping cart.)[5] - A view queries the model in order to generate an appropriate - user interface (for example, the view lists the shopping cart's - contents). Note that the view gets its own data from the model. + A view queries the model in order to generate an appropriate user + interface (for example, the view lists the shopping cart's contents). Note + that the view gets its own data from the model. - The controller may (in some implementations) issue a general - instruction to the view to render itself. In others, the view is - automatically notified by the model of changes in state - (Observer) which require a screen update. + The controller may (in some implementations) issue a general instruction + to the view to render itself. In others, the view is automatically + notified by the model of changes in state (Observer) which require a + screen update. - The user interface waits for further user interactions, which - restarts the cycle. + The user interface waits for further user interactions, which restarts the + cycle. To the author, it seems as if someone edited this Wikipedia definition, tortuously couching concepts in the most generic terms possible in order to -account for the use of the term "MVC" by current web frameworks. I doubt -such a broad definition would ever be agreed to by the original authors of -the MVC pattern. But *even so*, it seems most "MVC" web frameworks fail to -meet even this falsely generic definition. +account for the use of the term "MVC" by current web frameworks. I doubt such +a broad definition would ever be agreed to by the original authors of the MVC +pattern. But *even so*, it seems most MVC web frameworks fail to meet even +this falsely generic definition. For example, do your templates (views) always query models directly as is -claimed in "note that the view gets its own data from the model"? Probably -not. My "controllers" tend to do this, massaging the data for easier use by -the "view" (template). What do you do when your "controller" returns JSON? Do -your controllers use a template to generate JSON? If not, what's the "view" -then? Most MVC-style GUI web frameworks have some sort of event system -hooked up that lets the view detect when the model changes. The web just has -no such facility in its current form: it's effectively pull-only. - -So, in the interest of not mistaking desire with reality, and instead of -trying to jam the square peg that is the web into the round hole of "MVC", we -just punt and say there are two things: resources and views. The resource -tree represents a site structure, the view presents a resource. The -templates are really just an implementation detail of any given view: a view -doesn't need a template to return a response. There's no "controller": it -just doesn't exist. The "model" is either represented by the resource tree -or by a "domain model" (like a SQLAlchemy model) that is separate from the -framework entirely. This seems to us like more reasonable terminology, given -the current constraints of the web. +claimed in "note that the view gets its own data from the model"? Probably not. +My "controllers" tend to do this, massaging the data for easier use by the +"view" (template). What do you do when your "controller" returns JSON? Do your +controllers use a template to generate JSON? If not, what's the "view" then? +Most MVC-style GUI web frameworks have some sort of event system hooked up that +lets the view detect when the model changes. The web just has no such facility +in its current form; it's effectively pull-only. + +So, in the interest of not mistaking desire with reality, and instead of trying +to jam the square peg that is the web into the round hole of "MVC", we just +punt and say there are two things: resources and views. The resource tree +represents a site structure, the view presents a resource. The templates are +really just an implementation detail of any given view. A view doesn't need a +template to return a response. There's no "controller"; it just doesn't exist. +The "model" is either represented by the resource tree or by a "domain model" +(like an SQLAlchemy model) that is separate from the framework entirely. This +seems to us like more reasonable terminology, given the current constraints of +the web. + .. _apps_are_extensible: -Pyramid Applications are Extensible; I Don't Believe In Application Extensibility +Pyramid Applications Are Extensible; I Don't Believe in Application Extensibility --------------------------------------------------------------------------------- Any :app:`Pyramid` application written obeying certain constraints is *extensible*. This feature is discussed in the :app:`Pyramid` documentation -chapters named :ref:`extending_chapter` and :ref:`advconfig_narr`. It is -made possible by the use of the :term:`Zope Component Architecture` and -within :app:`Pyramid`. +chapters named :ref:`extending_chapter` and :ref:`advconfig_narr`. It is made +possible by the use of the :term:`Zope Component Architecture` within +:app:`Pyramid`. -"Extensible", in this context, means: +"Extensible" in this context means: -- The behavior of an application can be overridden or extended in a - particular *deployment* of the application without requiring that - the deployer modify the source of the original application. +- The behavior of an application can be overridden or extended in a particular + *deployment* of the application without requiring that the deployer modify + the source of the original application. -- The original developer is not required to anticipate any - extensibility plugpoints at application creation time to allow - fundamental application behavior to be overriden or extended. +- The original developer is not required to anticipate any extensibility + plug points at application creation time to allow fundamental application + behavior to be overridden or extended. - The original developer may optionally choose to anticipate an - application-specific set of plugpoints, which may be hooked by - a deployer. If he chooses to use the facilities provided by the - ZCA, the original developer does not need to think terribly hard - about the mechanics of introducing such a plugpoint. - -Many developers seem to believe that creating extensible applications is "not -worth it". They instead suggest that modifying the source of a given -application for each deployment to override behavior is more reasonable. -Much discussion about version control branching and merging typically ensues. - -It's clear that making every application extensible isn't required. The -majority of web applications only have a single deployment, and thus needn't -be extensible at all. However, some web applications have multiple -deployments, and some have *many* deployments. For example, a generic -"content management" system (CMS) may have basic functionality that needs to -be extended for a particular deployment. That CMS system may be deployed for -many organizations at many places. Some number of deployments of this CMS -may be deployed centrally by a third party and managed as a group. It's -useful to be able to extend such a system for each deployment via preordained -plugpoints than it is to continually keep each software branch of the system -in sync with some upstream source: the upstream developers may change code in -such a way that your changes to the same codebase conflict with theirs in -fiddly, trivial ways. Merging such changes repeatedly over the lifetime of a -deployment can be difficult and time consuming, and it's often useful to be -able to modify an application for a particular deployment in a less invasive -way. + application-specific set of plug points, which may be hooked by a deployer. + If they choose to use the facilities provided by the ZCA, the original + developer does not need to think terribly hard about the mechanics of + introducing such a plug point. + +Many developers seem to believe that creating extensible applications is not +worth it. They instead suggest that modifying the source of a given application +for each deployment to override behavior is more reasonable. Much discussion +about version control branching and merging typically ensues. + +It's clear that making every application extensible isn't required. The +majority of web applications only have a single deployment, and thus needn't be +extensible at all. However some web applications have multiple deployments, and +others have *many* deployments. For example, a generic content management +system (CMS) may have basic functionality that needs to be extended for a +particular deployment. That CMS may be deployed for many organizations at many +places. Some number of deployments of this CMS may be deployed centrally by a +third party and managed as a group. It's easier to be able to extend such a +system for each deployment via preordained plug points than it is to +continually keep each software branch of the system in sync with some upstream +source. The upstream developers may change code in such a way that your changes +to the same codebase conflict with theirs in fiddly, trivial ways. Merging such +changes repeatedly over the lifetime of a deployment can be difficult and time +consuming, and it's often useful to be able to modify an application for a +particular deployment in a less invasive way. If you don't want to think about :app:`Pyramid` application extensibility at -all, you needn't. You can ignore extensibility entirely. However, if you -follow the set of rules defined in :ref:`extending_chapter`, you don't need -to *make* your application extensible: any application you write in the -framework just *is* automatically extensible at a basic level. The -mechanisms that deployers use to extend it will be necessarily coarse: -typically, views, routes, and resources will be capable of being -overridden. But for most minor (and even some major) customizations, these -are often the only override plugpoints necessary: if the application doesn't -do exactly what the deployment requires, it's often possible for a deployer -to override a view, route, or resource and quickly make it do what he or she -wants it to do in ways *not necessarily anticipated by the original -developer*. Here are some example scenarios demonstrating the benefits of -such a feature. - -- If a deployment needs a different styling, the deployer may override the - main template and the CSS in a separate Python package which defines - overrides. - -- If a deployment needs an application page to do something differently needs - it to expose more or different information, the deployer may override the - view that renders the page within a separate Python package. +all, you needn't. You can ignore extensibility entirely. However if you follow +the set of rules defined in :ref:`extending_chapter`, you don't need to *make* +your application extensible. Any application you write in the framework just +*is* automatically extensible at a basic level. The mechanisms that deployers +use to extend it will be necessarily coarse. Typically views, routes, and +resources will be capable of being overridden. But for most minor (and even +some major) customizations, these are often the only override plug points +necessary. If the application doesn't do exactly what the deployment requires, +it's often possible for a deployer to override a view, route, or resource, and +quickly make it do what they want it to do in ways *not necessarily anticipated +by the original developer*. Here are some example scenarios demonstrating the +benefits of such a feature. + +- If a deployment needs a different styling, the deployer may override the main + template and the CSS in a separate Python package which defines overrides. + +- If a deployment needs an application page to do something differently, or to + expose more or different information, the deployer may override the view that + renders the page within a separate Python package. - If a deployment needs an additional feature, the deployer may add a view to the override package. -As long as the fundamental design of the upstream package doesn't change, -these types of modifications often survive across many releases of the -upstream package without needing to be revisited. +As long as the fundamental design of the upstream package doesn't change, these +types of modifications often survive across many releases of the upstream +package without needing to be revisited. Extending an application externally is not a panacea, and carries a set of -risks similar to branching and merging: sometimes major changes upstream will -cause you to need to revisit and update some of your modifications. But you -won't regularly need to deal wth meaningless textual merge conflicts that -trivial changes to upstream packages often entail when it comes time to -update the upstream package, because if you extend an application externally, -there just is no textual merge done. Your modifications will also, for -whatever its worth, be contained in one, canonical, well-defined place. +risks similar to branching and merging. Sometimes major changes upstream will +cause you to revisit and update some of your modifications. But you won't +regularly need to deal with meaningless textual merge conflicts that trivial +changes to upstream packages often entail when it comes time to update the +upstream package, because if you extend an application externally, there just +is no textual merge done. Your modifications will also, for whatever it's +worth, be contained in one, canonical, well-defined place. Branching an application and continually merging in order to get new features -and bugfixes is clearly useful. You can do that with a :app:`Pyramid` -application just as usefully as you can do it with any application. But +and bug fixes is clearly useful. You can do that with a :app:`Pyramid` +application just as usefully as you can do it with any application. But deployment of an application written in :app:`Pyramid` makes it possible to -avoid the need for this even if the application doesn't define any plugpoints -ahead of time. It's possible that promoters of competing web frameworks -dismiss this feature in favor of branching and merging because applications -written in their framework of choice aren't extensible out of the box in a -comparably fundamental way. +avoid the need for this even if the application doesn't define any plug points +ahead of time. It's possible that promoters of competing web frameworks dismiss +this feature in favor of branching and merging because applications written in +their framework of choice aren't extensible out of the box in a comparably +fundamental way. -While :app:`Pyramid` application are fundamentally extensible even if you +While :app:`Pyramid` applications are fundamentally extensible even if you don't write them with specific extensibility in mind, if you're moderately -adventurous, you can also take it a step further. If you learn more about -the :term:`Zope Component Architecture`, you can optionally use it to expose -other more domain-specific configuration plugpoints while developing an -application. The plugpoints you expose needn't be as coarse as the ones -provided automatically by :app:`Pyramid` itself. For example, you might -compose your own directive that configures a set of views for a prebaked -purpose (e.g. ``restview`` or somesuch) , allowing other people to refer to -that directive when they make declarations in the ``includeme`` of their -customization package. There is a cost for this: the developer of an -application that defines custom plugpoints for its deployers will need to -understand the ZCA or he will need to develop his own similar extensibility -system. - -Ultimately, any argument about whether the extensibility features lent to -applications by :app:`Pyramid` are "good" or "bad" is mostly pointless. You -needn't take advantage of the extensibility features provided by a particular +adventurous, you can also take it a step further. If you learn more about the +:term:`Zope Component Architecture`, you can optionally use it to expose other +more domain-specific configuration plug points while developing an application. +The plug points you expose needn't be as coarse as the ones provided +automatically by :app:`Pyramid` itself. For example, you might compose your own +directive that configures a set of views for a pre-baked purpose (e.g., +``restview`` or somesuch), allowing other people to refer to that directive +when they make declarations in the ``includeme`` of their customization +package. There is a cost for this: the developer of an application that defines +custom plug points for its deployers will need to understand the ZCA or they +will need to develop their own similar extensibility system. + +Ultimately any argument about whether the extensibility features lent to +applications by :app:`Pyramid` are good or bad is mostly pointless. You needn't +take advantage of the extensibility features provided by a particular :app:`Pyramid` application in order to affect a modification for a particular -set of its deployments. You can ignore the application's extensibility -plugpoints entirely, and instead use version control branching and merging to -manage application deployment modifications instead, as if you were deploying -an application written using any other web framework. +set of its deployments. You can ignore the application's extensibility plug +points entirely, and use version control branching and merging to manage +application deployment modifications instead, as if you were deploying an +application written using any other web framework. -Zope 3 Enforces "TTW" Authorization Checks By Default; Pyramid Does Not + +Zope 3 Enforces "TTW" Authorization Checks by Default; Pyramid Does Not ----------------------------------------------------------------------- Challenge +++++++++ :app:`Pyramid` performs automatic authorization checks only at :term:`view` -execution time. Zope 3 wraps context objects with a `security proxy -<http://wiki.zope.org/zope3/WhatAreSecurityProxies>`_, which causes Zope 3 to -do also security checks during attribute access. I like this, because it -means: +execution time. Zope 3 wraps context objects with a `security proxy +<http://wiki.zope.org/zope3/WhatAreSecurityProxies>`_, which causes Zope 3 also +to do security checks during attribute access. I like this, because it means: #) When I use the security proxy machinery, I can have a view that conditionally displays certain HTML elements (like form fields) or - prevents certain attributes from being modified depending on the the + prevents certain attributes from being modified depending on the permissions that the accessing user possesses with respect to a context object. @@ -983,37 +866,82 @@ Defense +++++++ :app:`Pyramid` was developed by folks familiar with Zope 2, which has a -"through the web" security model. This "TTW" security model was the -precursor to Zope 3's security proxies. Over time, as the :app:`Pyramid` -developers (working in Zope 2) created such sites, we found authorization -checks during code interpretation extremely useful in a minority of projects. -But much of the time, TTW authorization checks usually slowed down the -development velocity of projects that had no delegation requirements. In -particular, if we weren't allowing "untrusted" users to write arbitrary -Python code to be executed by our application, the burden of "through the -web" security checks proved too costly to justify. We (collectively) haven't -written an application on top of which untrusted developers are allowed to -write code in many years, so it seemed to make sense to drop this model by -default in a new web framework. +"through the web" security model. This TTW security model was the precursor +to Zope 3's security proxies. Over time, as the :app:`Pyramid` developers +(working in Zope 2) created such sites, we found authorization checks during +code interpretation extremely useful in a minority of projects. But much of +the time, TTW authorization checks usually slowed down the development +velocity of projects that had no delegation requirements. In particular, if +we weren't allowing untrusted users to write arbitrary Python code to be +executed by our application, the burden of through the web security checks +proved too costly to justify. We (collectively) haven't written an +application on top of which untrusted developers are allowed to write code in +many years, so it seemed to make sense to drop this model by default in a new +web framework. And since we tend to use the same toolkit for all web applications, it's just never been a concern to be able to use the same set of restricted-execution -code under two web different frameworks. +code under two different web frameworks. Justifications for disabling security proxies by default notwithstanding, -given that Zope 3 security proxies are "viral" by nature, the only -requirement to use one is to make sure you wrap a single object in a security -proxy and make sure to access that object normally when you want proxy -security checks to happen. It is possible to override the :app:`Pyramid` -"traverser" for a given application (see :ref:`changing_the_traverser`). To -get Zope3-like behavior, it is possible to plug in a different traverser -which returns Zope3-security-proxy-wrapped objects for each traversed object -(including the :term:`context` and the :term:`root`). This would have the -effect of creating a more Zope3-like environment without much effort. +given that Zope 3 security proxies are viral by nature, the only requirement +to use one is to make sure you wrap a single object in a security proxy and +make sure to access that object normally when you want proxy security checks +to happen. It is possible to override the :app:`Pyramid` traverser for a +given application (see :ref:`changing_the_traverser`). To get Zope3-like +behavior, it is possible to plug in a different traverser which returns +Zope3-security-proxy-wrapped objects for each traversed object (including the +:term:`context` and the :term:`root`). This would have the effect of +creating a more Zope3-like environment without much effort. + + +.. _http_exception_hierarchy: + +Pyramid uses its own HTTP exception class hierarchy rather than :mod:`webob.exc` +-------------------------------------------------------------------------------- + +.. versionadded:: 1.1 + +The HTTP exception classes defined in :mod:`pyramid.httpexceptions` are very +much like the ones defined in :mod:`webob.exc`, (e.g., +:class:`~pyramid.httpexceptions.HTTPNotFound` or +:class:`~pyramid.httpexceptions.HTTPForbidden`). They have the same names and +largely the same behavior, and all have a very similar implementation, but not +the same identity. Here's why they have a separate identity. + +- Making them separate allows the HTTP exception classes to subclass + :class:`pyramid.response.Response`. This speeds up response generation + slightly due to the way the Pyramid router works. The same speed up could be + gained by monkeypatching :class:`webob.response.Response`, but it's usually + the case that monkeypatching turns out to be evil and wrong. + +- Making them separate allows them to provide alternate ``__call__`` logic, + which also speeds up response generation. + +- Making them separate allows the exception classes to provide for the proper + value of ``RequestClass`` (:class:`pyramid.request.Request`). + +- Making them separate gives us freedom from thinking about backwards + compatibility code present in :mod:`webob.exc` related to Python 2.4, which + we no longer support in Pyramid 1.1+. + +- We change the behavior of two classes + (:class:`~pyramid.httpexceptions.HTTPNotFound` and + :class:`~pyramid.httpexceptions.HTTPForbidden`) in the module so that they + can be used by Pyramid internally for ``notfound`` and ``forbidden`` + exceptions. + +- Making them separate allows us to influence the docstrings of the exception + classes to provide Pyramid-specific documentation. + +- Making them separate allows us to silence a stupid deprecation warning under + Python 2.6 when the response objects are used as exceptions (related to + ``self.message``). + .. _simpler_traversal_model: -Pyramid has Simpler Traversal Machinery than Does Zope +Pyramid has simpler traversal machinery than does Zope ------------------------------------------------------ Zope's default traverser: @@ -1021,35 +949,35 @@ Zope's default traverser: - Allows developers to mutate the traversal name stack while traversing (they can add and remove path elements). -- Attempts to use an adaptation to obtain the "next" element in the path from +- Attempts to use an adaptation to obtain the next element in the path from the currently traversed object, falling back to ``__bobo_traverse__``, - ``__getitem__`` and eventually ``__getattr__``. + ``__getitem__``, and eventually ``__getattr__``. Zope's default traverser allows developers to mutate the traversal name stack -during traversal by mutating ``REQUEST['TraversalNameStack']``. Pyramid's -default traverser (``pyramid.traversal.ResourceTreeTraverser``) does not -offer a way to do this; it does not maintain a stack as a request attribute -and, even if it did, it does not pass the request to resource objects while -it's traversing. While it was handy at times, this feature was abused in -frameworks built atop Zope (like CMF and Plone), often making it difficult to -tell exactly what was happening when a traversal didn't match a view. I felt -it was better to make folks that wanted the feature replace the traverser -rather than build that particular honey pot in to the default traverser. +during traversal by mutating ``REQUEST['TraversalNameStack']``. Pyramid's +default traverser (``pyramid.traversal.ResourceTreeTraverser``) does not offer +a way to do this. It does not maintain a stack as a request attribute and, even +if it did, it does not pass the request to resource objects while it's +traversing. While it was handy at times, this feature was abused in frameworks +built atop Zope (like CMF and Plone), often making it difficult to tell exactly +what was happening when a traversal didn't match a view. I felt it was better +for folks that wanted the feature to make them replace the traverser rather +than build that particular honey pot in to the default traverser. Zope uses multiple mechanisms to attempt to obtain the next element in the resource tree based on a name. It first tries an adaptation of the current -resource to ``ITraversable``, and if that fails, it falls back to attempting +resource to ``ITraversable``, and if that fails, it falls back to attempting a number of magic methods on the resource (``__bobo_traverse__``, -``__getitem__``, and ``__getattr__``). My experience while both using Zope -and attempting to reimplement its publisher in ``repoze.zope2`` led me to -believe the following: +``__getitem__``, and ``__getattr__``). My experience while both using Zope and +attempting to reimplement its publisher in ``repoze.zope2`` led me to believe +the following: - The *default* traverser should be as simple as possible. Zope's publisher is somewhat difficult to follow and replicate due to the fallbacks it tried when one traversal method failed. It is also slow. - The *entire traverser* should be replaceable, not just elements of the - traversal machinery. Pyramid has a few "big" components rather than a + traversal machinery. Pyramid has a few big components rather than a plethora of small ones. If the entire traverser is replaceable, it's an antipattern to make portions of the default traverser replaceable. Doing so is a "knobs on knobs" pattern, which is unfortunately somewhat endemic @@ -1062,49 +990,52 @@ believe the following: to either replace the larger component entirely or turn knobs on the default implementation of the larger component, no one understands when (or whether) they should ever override the larger component entrirely. This - results, over time, in a "rusting together" of the larger "replaceable" - component and the framework itself, because people come to depend on the + results, over time, in a rusting together of the larger "replaceable" + component and the framework itself because people come to depend on the availability of the default component in order just to turn its knobs. The default component effectively becomes part of the framework, which entirely subverts the goal of making it replaceable. In Pyramid, typically if a - component is replaceable, it will itself have no knobs (it will be "solid - state"). If you want to influence behavior controlled by that component, + component is replaceable, it will itself have no knobs (it will be solid + state). If you want to influence behavior controlled by that component, you will replace the component instead of turning knobs attached to the component. + .. _microframeworks_smaller_hello_world: -Microframeworks Have Smaller Hello World Programs +Microframeworks have smaller Hello World programs ------------------------------------------------- -Self-described "microframeworks" exist: `Bottle <http://bottle.paws.de>`_ and -`Flask <http://flask.pocoo.org/>`_ are two that are becoming popular. `Bobo -<http://bobo.digicool.com/>`_ doesn't describe itself as a microframework, -but its intended userbase is much the same. Many others exist. We've -actually even (only as a teaching tool, not as any sort of official project) -`created one using BFG <http://bfg.repoze.org/videos#groundhog1>`_ (the -precursor to Pyramid). Microframeworks are small frameworks with one common -feature: each allows its users to create a fully functional application that -lives in a single Python file. +Self-described "microframeworks" exist. `Bottle <http://bottle.paws.de>`_ and +`Flask <http://flask.pocoo.org/>`_ are two that are becoming popular. `Bobo +<http://bobo.digicool.com/>`_ 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>`_. The videos use BFG, +a precursor to Pyramid, but the resulting code is `available for Pyramid too +<https://github.com/Pylons/groundhog>`_). Microframeworks are small frameworks +with one common feature: each allows its users to create a fully functional +application that lives in a single Python file. Some developers and microframework authors point out that Pyramid's "hello -world" single-file program is longer (by about five lines) than the -equivalent program in their favorite microframework. Guilty as charged. +world" single-file program is longer (by about five lines) than the equivalent +program in their favorite microframework. Guilty as charged. + +This loss isn't for lack of trying. Pyramid is useful in the same circumstance +in which microframeworks claim dominance: single-file applications. But Pyramid +doesn't sacrifice its ability to credibly support larger applications in order +to achieve "hello world" lines of code parity with the current crop of +microframeworks. Pyramid's design instead tries to avoid some common pitfalls +associated with naive declarative configuration schemes. The subsections which +follow explain the rationale. -This loss isn't for lack of trying. Pyramid is useful in the same -circumstance in which microframeworks claim dominance: single-file -applications. But Pyramid doesn't sacrifice its ability to credibly support -larger applications in order to achieve hello-world LoC parity with the -current crop of microframeworks. Pyramid's design instead tries to avoid -some common pitfalls associated with naive declarative configuration schemes. -The subsections which follow explain the rationale. .. _you_dont_own_modulescope: -Application Programmers Don't Control The Module-Scope Codepath (Import-Time Side-Effects Are Evil) +Application programmers don't control the module-scope codepath (import-time side-effects are evil) +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Please imagine a directory structure with a set of Python files in it: +Imagine a directory structure with a set of Python files in it: .. code-block:: text @@ -1144,7 +1075,7 @@ The contents of ``app2.py``: The contents of ``config.py``: .. code-block:: python - :linenos: + :linenos: L = [] @@ -1152,13 +1083,13 @@ The contents of ``config.py``: L.append(func) return func -If we cd to the directory that holds these files and we run ``python app.py`` -given the directory structure and code above, what happens? Presumably, our -``decorator`` decorator will be used twice, once by the decorated function -``foo`` in ``app.py`` and once by the decorated function ``bar`` in -``app2.py``. Since each time the decorator is used, the list ``L`` in -``config.py`` is appended to, we'd expect a list with two elements to be -printed, right? Sadly, no: +If we ``cd`` to the directory that holds these files, and we run +``python app.py``, given the directory structure and code above, what happens? +Presumably, our ``decorator`` decorator will be used twice, once by the +decorated function ``foo`` in ``app.py``, and once by the decorated function +``bar`` in ``app2.py``. Since each time the decorator is used, the list ``L`` +in ``config.py`` is appended to, we'd expect a list with two elements to be +printed, right? Sadly, no: .. code-block:: text @@ -1168,21 +1099,21 @@ printed, right? Sadly, no: <function bar at 0x7f4ea41ab2a8>] By visual inspection, that outcome (three different functions in the list) -seems impossible. We only defined two functions and we decorated each of -those functions only once, so we believe that the ``decorator`` decorator -will only run twice. However, what we believe is wrong because the code at -module scope in our ``app.py`` module was *executed twice*. The code is +seems impossible. We defined only two functions, and we decorated each of those +functions only once, so we believe that the ``decorator`` decorator will run +only twice. However, what we believe is in fact wrong, because the code at +module scope in our ``app.py`` module was *executed twice*. The code is executed once when the script is run as ``__main__`` (via ``python app.py``), and then it is executed again when ``app2.py`` imports the same file as ``app``. -What does this have to do with our comparison to microframeworks? Many -microframeworks in the current crop (e.g. Bottle, Flask) encourage you to -attach configuration decorators to objects defined at module scope. These -decorators execute arbitrarily complex registration code which populates a -singleton registry that is a global defined in external Python module. This -is analogous to the above example: the "global registry" in the above example -is the list ``L``. +What does this have to do with our comparison to microframeworks? Many +microframeworks in the current crop (e.g., Bottle and Flask) encourage you to +attach configuration decorators to objects defined at module scope. These +decorators execute arbitrarily complex registration code, which populates a +singleton registry that is a global which is in turn defined in external Python +module. This is analogous to the above example: the "global registry" in the +above example is the list ``L``. Let's see what happens when we use the same pattern with the `Groundhog <https://github.com/Pylons/groundhog>`_ microframework. Replace the contents @@ -1235,41 +1166,39 @@ will be. The encouragement to use decorators which perform population of an external registry has an unintended consequence: the application developer now must -assert ownership of every codepath that executes Python module scope -code. Module-scope code is presumed by the current crop of decorator-based -microframeworks to execute once and only once; if it executes more than once, -weird things will start to happen. It is up to the application developer to -maintain this invariant. Unfortunately, however, in reality, this is an -impossible task, because, Python programmers *do not own the module scope -codepath, and never will*. Anyone who tries to sell you on the idea that -they do is simply mistaken. Test runners that you may want to use to run -your code's tests often perform imports of arbitrary code in strange orders -that manifest bugs like the one demonstrated above. API documentation -generation tools do the same. Some people even think it's safe to use the -Python ``reload`` command or delete objects from ``sys.modules``, each of -which has hilarious effects when used against code that has import-time side -effects. - -Global-registry-mutating microframework programmers therefore will at some -point need to start reading the tea leaves about what *might* happen if -module scope code gets executed more than once like we do in the previous -paragraph. When Python programmers assume they can use the module-scope -codepath to run arbitrary code (especially code which populates an external -registry), and this assumption is challenged by reality, the application -developer is often required to undergo a painful, meticulous debugging -process to find the root cause of an inevitably obscure symptom. The -solution is often to rearrange application import ordering or move an import -statement from module-scope into a function body. The rationale for doing so -can never be expressed adequately in the checkin message which accompanies -the fix and can't be documented succinctly enough for the benefit of the rest -of the development team so that the problem never happens again. It will -happen again, especially if you are working on a project with other people -who haven't yet internalized the lessons you learned while you stepped -through module-scope code using ``pdb``. This is a really pretty poor -situation to find yourself in as an application developer: you probably -didn't even know your or your team signed up for the job, because the -documentation offered by decorator-based microframeworks don't warn you about -it. +assert ownership of every code path that executes Python module scope code. +Module-scope code is presumed by the current crop of decorator-based +microframeworks to execute once and only once. If it executes more than once, +weird things will start to happen. It is up to the application developer to +maintain this invariant. Unfortunately, in reality this is an impossible task, +because Python programmers *do not own the module scope code path, and never +will*. Anyone who tries to sell you on the idea that they do so is simply +mistaken. Test runners that you may want to use to run your code's tests often +perform imports of arbitrary code in strange orders that manifest bugs like the +one demonstrated above. API documentation generation tools do the same. Some +people even think it's safe to use the Python ``reload`` command, or delete +objects from ``sys.modules``, each of which has hilarious effects when used +against code that has import-time side effects. + +Global registry-mutating microframework programmers therefore will at some +point need to start reading the tea leaves about what *might* happen if module +scope code gets executed more than once, like we do in the previous paragraph. +When Python programmers assume they can use the module-scope code path to run +arbitrary code (especially code which populates an external registry), and this +assumption is challenged by reality, the application developer is often +required to undergo a painful, meticulous debugging process to find the root +cause of an inevitably obscure symptom. The solution is often to rearrange +application import ordering, or move an import statement from module-scope into +a function body. The rationale for doing so can never be expressed adequately +in the commit message which accompanies the fix, and can't be documented +succinctly enough for the benefit of the rest of the development team so that +the problem never happens again. It will happen again, especially if you are +working on a project with other people who haven't yet internalized the lessons +you learned while you stepped through module-scope code using ``pdb``. This is +a very poor situation in which to find yourself as an application developer: +you probably didn't even know you or your team signed up for the job, because +the documentation offered by decorator-based microframeworks don't warn you +about it. Folks who have a large investment in eager decorator-based configuration that populates an external data structure (such as microframework authors) may @@ -1284,23 +1213,23 @@ module-scope code will never happen. It will; it's just a matter of luck, time, and application complexity. If microframework authors do admit that the circumstance isn't contrived, -they might then argue that "real" damage will never happen as the result of -the double-execution (or triple-execution, etc) of module scope code. You -would be wise to disbelieve this assertion. The potential outcomes of -multiple execution are too numerous to predict because they involve delicate +they might then argue that real damage will never happen as the result of the +double-execution (or triple-execution, etc.) of module scope code. You would +be wise to disbelieve this assertion. The potential outcomes of multiple +execution are too numerous to predict because they involve delicate relationships between application and framework code as well as chronology of code execution. It's literally impossible for a framework author to know what will happen in all circumstances. But even if given the gift of omniscience for some limited set of circumstances, the framework author almost certainly does not have the double-execution anomaly in mind when -coding new features. He's thinking of adding a feature, not protecting +coding new features. They're thinking of adding a feature, not protecting against problems that might be caused by the 1% multiple execution case. However, any 1% case may cause 50% of your pain on a project, so it'd be nice -if it never occured. +if it never occurred. -Responsible microframeworks actually offer a back-door way around the -problem. They allow you to disuse decorator based configuration entirely. -Instead of requiring you to do the following: +Responsible microframeworks actually offer a back-door way around the problem. +They allow you to disuse decorator-based configuration entirely. Instead of +requiring you to do the following: .. code-block:: python :linenos: @@ -1314,7 +1243,7 @@ Instead of requiring you to do the following: if __name__ == '__main__': gh.run() -They allow you to disuse the decorator syntax and go almost-all-imperative: +They allow you to disuse the decorator syntax and go almost all-imperative: .. code-block:: python :linenos: @@ -1338,21 +1267,24 @@ predictability. .. note:: - Astute readers may notice that Pyramid has configuration decorators too. - Aha! Don't these decorators have the same problems? No. These decorators - do not populate an external Python module when they are executed. They - only mutate the functions (and classes and methods) they're attached to. - These mutations must later be found during a "scan" process that has a - predictable and structured import phase. Module-localized mutation is - actually the best-case circumstance for double-imports; if a module only - mutates itself and its contents at import time, if it is imported twice, - that's OK, because each decorator invocation will always be mutating an - independent copy of the object its attached to, not a shared resource like - a registry in another module. This has the effect that - double-registrations will never be performed. - -Routes (Usually) Need Relative Ordering -+++++++++++++++++++++++++++++++++++++++ + Astute readers may notice that Pyramid has configuration decorators too. Aha! + Don't these decorators have the same problems? No. These decorators do not + populate an external Python module when they are executed. They only mutate + the functions (and classes and methods) to which they're attached. These + mutations must later be found during a scan process that has a predictable + and structured import phase. Module-localized mutation is actually the + best-case circumstance for double-imports. If a module only mutates itself + and its contents at import time, if it is imported twice, that's OK, because + each decorator invocation will always be mutating an independent copy of the + object to which it's attached, not a shared resource like a registry in + another module. This has the effect that double-registrations will never be + performed. + + +.. _routes_need_ordering: + +Routes need relative ordering ++++++++++++++++++++++++++++++ Consider the following simple `Groundhog <https://github.com/Pylons/groundhog>`_ application: @@ -1378,9 +1310,9 @@ Consider the following simple `Groundhog if __name__ == '__main__': app.run() -If you run this application and visit the URL ``/admin``, you will see -"admin" page. This is the intended result. However, what if you rearrange -the order of the function definitions in the file? +If you run this application and visit the URL ``/admin``, you will see the +"admin" page. This is the intended result. However, what if you rearrange the +order of the function definitions in the file? .. code-block:: python :linenos: @@ -1403,11 +1335,11 @@ the order of the function definitions in the file? if __name__ == '__main__': app.run() -If you run this application and visit the URL ``/admin``, you will now be -returned a 404 error. This is probably not what you intended. The reason -you see a 404 error when you rearrange function definition ordering is that -routing declarations expressed via our microframework's routing decorators -have an *ordering*, and that ordering matters. +If you run this application and visit the URL ``/admin``, your app will now +return a 404 error. This is probably not what you intended. The reason you see +a 404 error when you rearrange function definition ordering is that routing +declarations expressed via our microframework's routing decorators have an +*ordering*, and that ordering matters. In the first case, where we achieved the expected result, we first added a route with the pattern ``/admin``, then we added a route with the pattern @@ -1415,38 +1347,71 @@ route with the pattern ``/admin``, then we added a route with the pattern scope. When a request with a ``PATH_INFO`` of ``/admin`` enters our application, the web framework loops over each of our application's route patterns in the order in which they were defined in our module. As a result, -the view associated with the ``/admin`` routing pattern will be invoked: it -matches first. All is right with the world. +the view associated with the ``/admin`` routing pattern will be invoked because +it matches first. All is right with the world. In the second case, where we did not achieve the expected result, we first added a route with the pattern ``/:action``, then we added a route with the pattern ``/admin``. When a request with a ``PATH_INFO`` of ``/admin`` enters our application, the web framework loops over each of our application's route patterns in the order in which they were defined in our module. As a result, -the view associated with the ``/:action`` routing pattern will be invoked: it -matches first. A 404 error is raised. This is not what we wanted; it just -happened due to the order in which we defined our view functions. - -You may be willing to maintain an ordering of your view functions which -reifies your routing policy. Your application may be small enough where this -will never cause an issue. If it becomes large enough to matter, however, I -don't envy you. Maintaining that ordering as your application grows larger -will be difficult. At some point, you will also need to start controlling -*import* ordering as well as function definition ordering. When your -application grows beyond the size of a single file, and when decorators are -used to register views, the non-``__main__`` modules which contain -configuration decorators must be imported somehow for their configuration to -be executed. - -Does that make you a little uncomfortable? It should, because +the view associated with the ``/:action`` routing pattern will be invoked +because it matches first. A 404 error is raised. This is not what we wanted; it +just happened due to the order in which we defined our view functions. + +This is because Groundhog routes are added to the routing map in import order, +and matched in the same order when a request comes in. Bottle, like Groundhog, +as of this writing, matches routes in the order in which they're defined at +Python execution time. Flask, on the other hand, does not order route matching +based on import order. Instead it reorders the routes you add to your +application based on their "complexity". Other microframeworks have varying +strategies to do route ordering. + +Your application may be small enough where route ordering will never cause an +issue. If your application becomes large enough, however, being able to specify +or predict that ordering as your application grows larger will be difficult. +At some point, you will likely need to start controlling route ordering more +explicitly, especially in applications that require extensibility. + +If your microframework orders route matching based on complexity, you'll need +to understand what is meant by "complexity", and you'll need to attempt to +inject a "less complex" route to have it get matched before any "more complex" +one to ensure that it's tried first. + +If your microframework orders its route matching based on relative +import/execution of function decorator definitions, you will need to ensure +that you execute all of these statements in the "right" order, and you'll need +to be cognizant of this import/execution ordering as you grow your application +or try to extend it. This is a difficult invariant to maintain for all but the +smallest applications. + +In either case, your application must import the non-``__main__`` modules which +contain configuration decorations somehow for their configuration to be +executed. Does that make you a little uncomfortable? It should, because :ref:`you_dont_own_modulescope`. -"Stacked Object Proxies" Are Too Clever / Thread Locals Are A Nuisance +Pyramid uses neither decorator import time ordering nor does it attempt to +divine the relative complexity of one route to another as a means to define a +route match ordering. In Pyramid, you have to maintain relative route ordering +imperatively via the chronology of multiple executions of the +:meth:`pyramid.config.Configurator.add_route` method. The order in which you +repeatedly call ``add_route`` becomes the order of route matching. + +If needing to maintain this imperative ordering truly bugs you, you can use +:term:`traversal` instead of route matching, which is a completely declarative +(and completely predictable) mechanism to map code to URLs. While URL dispatch +is easier to understand for small non-extensible applications, traversal is a +great fit for very large applications and applications that need to be +arbitrarily extensible. + + +.. _thread_local_nuisance: + +"Stacked object proxies" are too clever / thread locals are a nuisance ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -In another manifestation of "import fascination", some microframeworks use -the ``import`` statement to get a handle to an object which *is not logically -global*: +Some microframeworks use the ``import`` statement to get a handle to an +object which *is not logically global*: .. code-block:: python :linenos: @@ -1471,7 +1436,7 @@ of this discussion, I'll do so as well. Import statements in Python (``import foo``, ``from bar import baz``) are most frequently performed to obtain a reference to an object defined globally -within an external Python module. However, in "normal" programs, they are +within an external Python module. However, in normal programs, they are never used to obtain a reference to an object that has a lifetime measured by the scope of the body of a function. It would be absurd to try to import, for example, a variable named ``i`` representing a loop counter defined in @@ -1483,34 +1448,37 @@ code below: def afunc(): for i in range(10): - print i + print(i) -By its nature, the *request* object created as the result of a WSGI server's -call into a long-lived web framework cannot be global, because the lifetime -of a single request will be much shorter than the lifetime of the process -running the framework. A request object created by a web framework actually -has more similarity to the ``i`` loop counter in our example above than it -has to any comparable importable object defined in the Python standard -library or in "normal" library code. +By its nature, the *request* object that is created as the result of a WSGI +server's call into a long-lived web framework cannot be global, because the +lifetime of a single request will be much shorter than the lifetime of the +process running the framework. A request object created by a web framework +actually has more similarity to the ``i`` loop counter in our example above +than it has to any comparable importable object defined in the Python standard +library or in normal library code. However, systems which use stacked object proxies promote locally scoped -objects such as ``request`` out to module scope, for the purpose of being -able to offer users a "nice" spelling involving ``import``. They, for what I -consider dubious reasons, would rather present to their users the canonical -way of getting at a ``request`` as ``from framework import request`` instead -of a saner ``from myframework.threadlocals import get_request; request = -get_request()`` even though the latter is more explicit. +objects, such as ``request``, out to module scope, for the purpose of being +able to offer users a nice spelling involving ``import``. They, for what I +consider dubious reasons, would rather present to their users the canonical way +of getting at a ``request`` as ``from framework import request`` instead of a +saner ``from myframework.threadlocals import get_request; request = +get_request()``, even though the latter is more explicit. It would be *most* explicit if the microframeworks did not use thread local -variables at all. Pyramid view functions are passed a request object; many -of Pyramid's APIs require that an explicit request object be passed to them. -It is *possible* to retrieve the current Pyramid request as a threadlocal -variable but it is a "in case of emergency, break glass" type of activity. -This explicitness makes Pyramid view functions more easily unit testable, as -you don't need to rely on the framework to manufacture suitable "dummy" -request (and other similarly-scoped) objects during test setup. It also -makes them more likely to work on arbitrary systems, such as async servers -that do no monkeypatching. +variables at all. Pyramid view functions are passed a request object. Many of +Pyramid's APIs require that an explicit request object be passed to them. It is +*possible* to retrieve the current Pyramid request as a threadlocal variable, +but it is an "in case of emergency, break glass" type of activity. This +explicitness makes Pyramid view functions more easily unit testable, as you +don't need to rely on the framework to manufacture suitable "dummy" request +(and other similarly-scoped) objects during test setup. It also makes them +more likely to work on arbitrary systems, such as async servers, that do no +monkeypatching. + + +.. _explicitly_wsgi: Explicitly WSGI +++++++++++++++ @@ -1524,71 +1492,73 @@ import a WSGI server and use it to serve up their Pyramid application as per the documentation of that WSGI server. The extra lines saved by abstracting away the serving step behind ``run()`` -seem to have driven dubious second-order decisions related to API in some -microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass -for each type of WSGI server it supports via its ``app.run()`` mechanism. -This means that there exists code in ``bottle.py`` that depends on the -following modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``, +seems to have driven dubious second-order decisions related to its API in some +microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass for +each type of WSGI server it supports via its ``app.run()`` mechanism. This +means that there exists code in ``bottle.py`` that depends on the following +modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``, ``tornado``, ``google.appengine``, ``twisted.web``, ``diesel``, ``gevent``, -``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server -you want to run by passing its name into the ``run`` method. In theory, this -sounds great: I can try Bottle out on ``gunicorn`` just by passing in a name! -However, to fully test Bottle, all of these third-party systems must be -installed and functional; the Bottle developers must monitor changes to each -of these packages and make sure their code still interfaces properly with -them. This expands the packages required for testing greatly; this is a -*lot* of requirements. It is likely difficult to fully automate these tests -due to requirements conflicts and build issues. +``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server you +want to run by passing its name into the ``run`` method. In theory, this sounds +great: I can try out Bottle on ``gunicorn`` just by passing in a name! However, +to fully test Bottle, all of these third-party systems must be installed and +functional. The Bottle developers must monitor changes to each of these +packages and make sure their code still interfaces properly with them. This +increases the number of packages required for testing greatly; this is a *lot* +of requirements. It is likely difficult to fully automate these tests due to +requirements conflicts and build issues. As a result, for single-file apps, we currently don't bother to offer a -``run()`` shortcut; we tell folks to import their WSGI server of choice and -run it "by hand". For the people who want a server abstraction layer, we -suggest that they use PasteDeploy. In PasteDeploy-based systems, the onus -for making sure that the server can interface with a WSGI application is -placed on the server developer, not the web framework developer, making it -more likely to be timely and correct. - -Wrapping Up +``run()`` shortcut. We tell folks to import their WSGI server of choice and run +it by hand. For the people who want a server abstraction layer, we suggest that +they use PasteDeploy. In PasteDeploy-based systems, the onus for making sure +that the server can interface with a WSGI application is placed on the server +developer, not the web framework developer, making it more likely to be timely +and correct. + +Wrapping up +++++++++++ -Here's a diagrammed version of the simplest pyramid application, where -comments take into account what we've discussed in the +Here's a diagrammed version of the simplest pyramid application, where the +inlined comments take into account what we've discussed in the :ref:`microframeworks_smaller_hello_world` section. .. code-block:: python :linenos: - from webob import Response # explicit response objects, no TL - from paste.httpserver import serve # explicitly WSGI + from pyramid.response import Response # explicit response, no thread local + from wsgiref.simple_server import make_server # explicitly WSGI def hello_world(request): # accepts a request; no request thread local reqd # explicit response object means no response threadlocal - return Response('Hello world!') + return Response('Hello world!') if __name__ == '__main__': from pyramid.config import Configurator - config = Configurator() # no global application object. + config = Configurator() # no global application object config.add_view(hello_world) # explicit non-decorator registration app = config.make_wsgi_app() # explicitly WSGI - serve(app, host='0.0.0.0') # explicitly WSGI + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() # explicitly WSGI + -Pyramid Doesn't Offer Pluggable Apps +Pyramid doesn't offer pluggable apps ------------------------------------ It is "Pyramidic" to compose multiple external sources into the same -configuration using :meth:`~pyramid.config.Configuration.include`. Any +configuration using :meth:`~pyramid.config.Configurator.include`. Any number of includes can be done to compose an application; includes can even be done from within other includes. Any directive can be used within an include that can be used outside of one (such as -:meth:`~pyramid.config.Configurator.add_view`, etc). +:meth:`~pyramid.config.Configurator.add_view`). -Pyramid has a conflict detection system that will throw an error if two -included externals try to add "the same" configuration in a conflicting -way (such as both externals trying to add a route using the same name, -or both externals trying to add a view with the same set of predicates). -It's awful tempting to call this set of features something that can be -used to compose a system out of "pluggable applications". But in -reality, there are a number of problems with claiming this: +Pyramid has a conflict detection system that will throw an error if two +included externals try to add the same configuration in a conflicting way +(such as both externals trying to add a route using the same name, or both +externals trying to add a view with the same set of predicates). It's awful +tempting to call this set of features something that can be used to compose a +system out of "pluggable applications". But in reality, there are a number +of problems with claiming this: - The terminology is strained. Pyramid really has no notion of a plurality of "applications", just a way to compose configuration @@ -1599,31 +1569,28 @@ reality, there are a number of problems with claiming this: the boundaries of one "application" (in the sense of configuration from an external that adds routes, views, etc) from another. -- Pyramid doesn't provide enough "rails" to make it possible to - integrate truly honest-to-god, download-an-app-from-a-random-place - and-plug-it-in-to-create-a-system "pluggable" applications. - Because Pyramid itself isn't opinionated (it doesn't mandate a - particular kind of database, it offers multiple ways to map URLs - to code, etc), it's unlikely that someone who creates something - "application-like" will be able to casually redistribute it - to J. Random Pyramid User and have it "just work" by asking him - to config.include a function from the package. - This is particularly true of very high level components such - as blogs, wikis, twitter clones, commenting systems, etc. - The "integrator" (the Pyramid developer who has downloaded a - package advertised as a "pluggable app") will almost certainly - have made different choices about e.g. what type of persistence - system he's using, and for the integrator to appease the - requirements of the "pluggable application", he may be required - to set up a different database, make changes to his own code - to prevent his application from "shadowing" the pluggable - app (or vice versa), and any other number of arbitrary - changes. +- Pyramid doesn't provide enough "rails" to make it possible to integrate + truly honest-to-god, download-an-app-from-a-random-place + and-plug-it-in-to-create-a-system "pluggable" applications. Because + Pyramid itself isn't opinionated (it doesn't mandate a particular kind of + database, it offers multiple ways to map URLs to code, etc), it's unlikely + that someone who creates something application-like will be able to + casually redistribute it to J. Random Pyramid User and have it just work by + asking him to config.include a function from the package. This is + particularly true of very high level components such as blogs, wikis, + twitter clones, commenting systems, etc. The integrator (the Pyramid + developer who has downloaded a package advertised as a "pluggable app") + will almost certainly have made different choices about e.g. what type of + persistence system he's using, and for the integrator to appease the + requirements of the "pluggable application", he may be required to set up a + different database, make changes to his own code to prevent his application + from shadowing the pluggable app (or vice versa), and any other number of + arbitrary changes. For this reason, we claim that Pyramid has "extensible" applications, not pluggable applications. Any Pyramid application can be extended without forking it as long as its configuration statements have been -composed into things that can be pulled in via "config.include". +composed into things that can be pulled in via ``config.include``. It's also perfectly reasonable for a single developer or team to create a set of interoperating components which can be enabled or disabled by using @@ -1639,10 +1606,10 @@ really work without local modification. Truly pluggable applications need to be created at a much higher level than a web framework, as no web framework can offer enough constraints to really -make them "work out of the box". They really need to plug into an -application, instead. It would be a noble goal to build an application with -Pyramid that provides these constraints and which truly does offer a way to -plug in applications (Joomla, Plone, Drupal come to mind). +make them work out of the box. They really need to plug into an application, +instead. It would be a noble goal to build an application with Pyramid that +provides these constraints and which truly does offer a way to plug in +applications (Joomla, Plone, Drupal come to mind). Pyramid Has Zope Things In It, So It's Too Complex -------------------------------------------------- @@ -1659,7 +1626,7 @@ reads something like this: (Paraphrased from a real email, actually.) -Let's take this criticism point-by point. +Let's take this criticism point-by-point. Too Complex +++++++++++ @@ -1669,7 +1636,7 @@ If you can understand this hello world program, you can use Pyramid: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -1680,12 +1647,13 @@ If you can understand this hello world program, you can use Pyramid: config = Configurator() config.add_view(hello_world) app = config.make_wsgi_app() - serve(app) + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() -Pyramid has ~ 650 pages of documentation (printed), covering topics from the +Pyramid has ~ 700 pages of documentation (printed), covering topics from the very basic to the most advanced. *Nothing* is left undocumented, quite literally. It also has an *awesome*, very helpful community. Visit the -#repoze and/or #pylons IRC channels on freenode.net and see. +#pyramid IRC channel on freenode.net (irc://freenode.net#pyramid) and see. Hate Zope +++++++++ @@ -1704,7 +1672,7 @@ was written to address these issues. If it's Zope3-the-web-framework, Pyramid is *definitely* not that. Making use of lots of Zope 3 technologies is territory already staked out by the :term:`Grok` project. Save for the obvious fact that they're both web -frameworks, :mod:`Pyramid` is very, very different than Grok. Grok exposes +frameworks, :app:`Pyramid` is very, very different than Grok. Grok exposes lots of Zope technologies to end users. On the other hand, if you need to understand a Zope-only concept while using Pyramid, then we've failed on some very basic axis. @@ -1717,7 +1685,7 @@ some sort of monolithic thing, and a lot of its software is usable externally. And while it's not really the job of this document to defend it, Zope has been around for over 10 years and has an incredibly large, active community. If you don't believe this, -http://taichino.appspot.com/pypi_ranking/authors is an eye-opening reality +http://pypi-ranking.info/author is an eye-opening reality check. Love Simplicity diff --git a/docs/foreword.rst b/docs/foreword.rst index aa8d7c77b..cc8271bdf 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -1,3 +1,5 @@ +:orphan: + Foreword ======== diff --git a/docs/glossary.rst b/docs/glossary.rst index 579d89afd..1d97bffe8 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -7,21 +7,32 @@ Glossary :sorted: request - A ``WebOb`` request object. See :ref:`webob_chapter` (narrative) - and :ref:`request_module` (API documentation) for information - about request objects. + An object that represents an HTTP request, usually an instance of the + :class:`pyramid.request.Request` class. See :ref:`webob_chapter` + (narrative) and :ref:`request_module` (API documentation) for + information about request objects. request factory - An object which, provided a WSGI environment as a single - positional argument, returns a ``WebOb`` compatible request. + An object which, provided a :term:`WSGI` environment as a single + positional argument, returns a Pyramid-compatible request. + + response factory + An object which, provided a :term:`request` as a single positional + argument, returns a Pyramid-compatible response. See + :class:`pyramid.interfaces.IResponseFactory`. response - An object that has three attributes: ``app_iter`` (representing an - iterable body), ``headerlist`` (representing the http headers sent - to the user agent), and ``status`` (representing the http status - string sent to the user agent). This is the interface defined for - ``WebOb`` response objects. See :ref:`webob_chapter` for - information about response objects. + An object returned by a :term:`view callable` that represents response + data returned to the requesting user agent. It must implement the + :class:`pyramid.interfaces.IResponse` interface. A response object is + typically an instance of the :class:`pyramid.response.Response` class or + a subclass such as :class:`pyramid.httpexceptions.HTTPFound`. See + :ref:`webob_chapter` for information about response objects. + + response adapter + A callable which accepts an arbitrary object and "converts" it to a + :class:`pyramid.response.Response` object. See :ref:`using_iresponse` + for more information. Repoze "Repoze" is essentially a "brand" of software developed by `Agendaless @@ -35,15 +46,25 @@ Glossary setuptools `Setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ builds on Python's ``distutils`` to provide easier building, - distribution, and installation of libraries and applications. + distribution, and installation of libraries and applications. As of + this writing, setuptools runs under Python 2, but not under Python 3. + You can use :term:`distribute` under Python 3 instead. + + distribute + `Distribute <http://packages.python.org/distribute/>`_ is a fork of + :term:`setuptools` which runs on both Python 2 and Python 3. pkg_resources - A module which ships with :term:`setuptools` that provides an API for - addressing "asset files" within a Python :term:`package`. Asset files - are static files, template files, etc; basically anything - non-Python-source that lives in a Python package can be considered a - asset file. See also `PkgResources - <http://peak.telecommunity.com/DevCenter/PkgResources>`_ + A module which ships with :term:`setuptools` and :term:`distribute` that + provides an API for addressing "asset files" within a Python + :term:`package`. Asset files are static files, template files, etc; + basically anything non-Python-source that lives in a Python package can + be considered a asset file. + + .. seealso:: + + See also `PkgResources + <http://peak.telecommunity.com/DevCenter/PkgResources>`_. asset Any file contained within a Python :term:`package` which is *not* @@ -78,7 +99,7 @@ Glossary (Setuptools/distutils terminology). A file representing an installable library or application. Distributions are usually files that have the suffix of ``.egg``, ``.tar.gz``, or ``.zip``. - Distributions are the target of Setuptools commands such as + Distributions are the target of Setuptools-related commands such as ``easy_install``. entry point @@ -89,7 +110,7 @@ Glossary dotted Python name A reference to a Python object by name using a string, in the form - ``path.to.modulename:attributename``. Often used in Paste and + ``path.to.modulename:attributename``. Often used in Pyramid and setuptools configurations. A variant is used in dotted names within configurator method arguments that name objects (such as the "add_view" method's "view" and "context" attributes): the colon (``:``) is not @@ -134,16 +155,19 @@ Glossary request before it returns a :term:`context` resource. virtualenv - An isolated Python environment. Allows you to control which - packages are used on a particular project by cloning your main - Python. `virtualenv <http://pypi.python.org/pypi/virtualenv>`_ - was created by Ian Bicking. + The `virtualenv tool <https://virtualenv.pypa.io/en/latest/>`_ that allows + one to create virtual environments. In Python 3.3 and greater, + :term:`venv` is the preferred tool. + + Note: whenever you encounter commands prefixed with ``$VENV`` (Unix) + or ``%VENV`` (Windows), know that that is the environment variable whose + value is the root of the virtual environment in question. resource An object representing a node in the :term:`resource tree` of an - application. If :mod:`traversal` is used, a resource is an element in + application. If :term:`traversal` is used, a resource is an element in the resource tree traversed by the system. When traversal is used, a - resource becomes the :term:`context` of a :term:`view`. If :mod:`url + resource becomes the :term:`context` of a :term:`view`. If :term:`url dispatch` is used, a single resource is generated for each request and is used as the context resource of a view. @@ -210,7 +234,7 @@ Glossary object *location-aware*. permission - A string or unicode object that represents an action being taken against + A string or Unicode object that represents an action being taken against a :term:`context` resource. A permission is associated with a view name and a resource type by the developer. Resources are decorated with security declarations (e.g. an :term:`ACL`), which reference these @@ -227,7 +251,11 @@ Glossary be effectively amended with a ``permission`` argument that will require that the executing user possess the default permission in order to successfully execute the associated :term:`view - callable` See also :ref:`setting_a_default_permission`. + callable`. + + .. seealso:: + + See also :ref:`setting_a_default_permission`. ACE An *access control entry*. An access control entry is one element @@ -245,7 +273,7 @@ Glossary (Allow, 'bob', 'read'), (Deny, 'fred', 'write')]``. If an ACL is attached to a resource instance, and that resource is findable via the context resource, it will be consulted any active security policy to - determine wither a particular request can be fulfilled given the + determine whether a particular request can be fulfilled given the :term:`authentication` information in the request. authentication @@ -263,13 +291,22 @@ Glossary :term:`authorization policy`. principal - A *principal* is a string or unicode object representing a userid - or a group id. It is provided by an :term:`authentication - policy`. For example, if a user had the user id "bob", and Bob - was part of two groups named "group foo" and "group bar", the - request might have information attached to it that would - indicate that Bob was represented by three principals: "bob", - "group foo" and "group bar". + A *principal* is a string or Unicode object representing an entity, + typically a user or group. Principals are provided by an + :term:`authentication policy`. For example, if a user has the + :term:`userid` `bob`, and is a member of two groups named `group foo` and + `group bar`, then the request might have information attached to it + indicating that Bob was represented by three principals: `bob`, `group + foo` and `group bar`. + + userid + A *userid* is a string or Unicode object used to identify and authenticate + a real-world user or client. A userid is supplied to an + :term:`authentication policy` in order to discover the user's + :term:`principals <principal>`. In the authentication policies which + :app:`Pyramid` provides, the default behavior returns the user's userid as + a principal, but this is not strictly necessary in custom policies that + define their principals differently. authorization policy An authorization policy in :app:`Pyramid` terms is a bit of @@ -284,7 +321,7 @@ Glossary :term:`principal` (or principals) associated with a request. WSGI - `Web Server Gateway Interface <http://wsgi.org/>`_. This is a + `Web Server Gateway Interface <http://www.wsgi.org/>`_. This is a Python standard for connecting web applications to web servers, similar to the concept of Java Servlets. :app:`Pyramid` requires that your application be served as a WSGI application. @@ -293,13 +330,13 @@ Glossary *Middleware* is a :term:`WSGI` concept. It is a WSGI component that acts both as a server and an application. Interesting uses for middleware exist, such as caching, content-transport - encoding, and other functions. See `WSGI.org <http://wsgi.org>`_ + encoding, and other functions. See `WSGI.org <http://www.wsgi.org>`_ or `PyPI <http://python.org/pypi>`_ to find middleware for your application. pipeline - The :term:`Paste` term for a single configuration of a WSGI - server, a WSGI application, with a set of middleware in-between. + The :term:`PasteDeploy` term for a single configuration of a WSGI + server, a WSGI application, with a set of :term:`middleware` in-between. Zope `The Z Object Publishing Framework <http://zope.org>`_, a @@ -312,42 +349,31 @@ Glossary `A full-featured Python web framework <http://djangoproject.com>`_. Pylons - `A lightweight Python web framework <http://pylonshq.com>`_ and a - predecessor of Pyramid. + `A lightweight Python web framework <http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/>`_ + and a predecessor of Pyramid. ZODB `Zope Object Database <http://zodb.org>`_, a persistent Python object store. - ZEO - `Zope Enterprise Objects - <http://www.zope.org/Documentation/Books/ZopeBook/2_6Edition/ZEO.stx>`_ - allows multiple simultaneous processes to access a single - :term:`ZODB` database. - WebOb - `WebOb <http://pythonpaste.org/webob/>`_ is a WSGI request/response + `WebOb <http://webob.org>`_ is a WSGI request/response library created by Ian Bicking. - Paste - `Paste <http://pythonpaste.org>`_ is a WSGI development and - deployment system developed by Ian Bicking. - PasteDeploy - `PasteDeploy <http://pythonpaste.org>`_ is a library used by + `PasteDeploy <http://pythonpaste.org/deploy/>`_ is a library used by :app:`Pyramid` which makes it possible to configure :term:`WSGI` components together declaratively within an ``.ini`` - file. It was developed by Ian Bicking as part of :term:`Paste`. + file. It was developed by Ian Bicking. Chameleon - `chameleon <http://chameleon.repoze.org>`_ is an attribute - language template compiler which supports both the :term:`ZPT` and - :term:`Genshi` templating specifications. It is written and - maintained by Malthe Borch. It has several extensions, such as - the ability to use bracketed (Genshi-style) ``${name}`` syntax, - even within ZPT. It is also much faster than the reference - implementations of both ZPT and Genshi. :app:`Pyramid` offers - Chameleon templating out of the box in ZPT and text flavors. + `chameleon <https://chameleon.readthedocs.org/en/latest/>`_ is an + attribute language template compiler which supports the :term:`ZPT` + templating specification. It is written and maintained by Malthe Borch. It + has several extensions, such as the ability to use bracketed (Mako-style) + ``${name}`` syntax. It is also much faster than the reference + implementation of ZPT. :app:`Pyramid` offers Chameleon templating out of + the box in ZPT and text flavors. ZPT The `Zope Page Template <http://wiki.zope.org/ZPT/FrontPage>`_ @@ -376,7 +402,11 @@ Glossary route A single pattern matched by the :term:`url dispatch` subsystem, which generally resolves to a :term:`root factory` (and then - ultimately a :term:`view`). See also :term:`url dispatch`. + ultimately a :term:`view`). + + .. seealso:: + + See also :term:`url dispatch`. route configuration Route configuration is the act of associating request parameters with a @@ -393,10 +423,9 @@ Glossary dispatching and other application configuration tasks. reStructuredText - A `plain text format <http://docutils.sourceforge.net/rst.html>`_ - that is the defacto standard for descriptive text shipped in - :term:`distribution` files, and Python docstrings. This - documentation is authored in ReStructuredText format. + A `plain text markup format <http://docutils.sourceforge.net/rst.html>`_ + that is the defacto standard for documenting Python projects. + The Pyramid documentation is written in reStructuredText. root The object at which :term:`traversal` begins when :app:`Pyramid` @@ -450,7 +479,7 @@ Glossary :term:`request` object that :app:`Pyramid` generates and manipulates has one or more :term:`interface` objects attached to it. The default interface attached to a request object is - ``pyramid.interfaces.IRequest``. + :class:`pyramid.interfaces.IRequest`. repoze.lemonade Zope2 CMF-like `data structures and helper facilities @@ -474,10 +503,24 @@ Glossary :app:`Pyramid` to form a workflow system. virtual root - A resource object representing the "virtual" root of a request; this - is typically the physical root object (the object returned by the - application root factory) unless :ref:`vhosting_chapter` is in - use. + A resource object representing the "virtual" root of a request; this is + typically the :term:`physical root` object unless :ref:`vhosting_chapter` + is in use. + + physical root + The object returned by the application :term:`root factory`. + Unlike the :term:`virtual root` of a request, it is not impacted by + :ref:`vhosting_chapter`: it will always be the actual object returned by + the root factory, never a subobject. + + physical path + The path required by a traversal which resolve a :term:`resource` starting + from the :term:`physical root`. For example, the physical path of the + ``abc`` subobject of the physical root object is ``/abc``. Physical paths + can also be specified as tuples where the first element is the empty + string (representing the root), and every other element is a Unicode + object, e.g. ``('', 'abc')``. Physical paths are also sometimes called + "traversal paths". lineage An ordered sequence of objects based on a ":term:`location` -aware" @@ -488,15 +531,20 @@ Glossary available as its ``__parent__`` attribute. root factory - The "root factory" of a :app:`Pyramid` application is called - on every request sent to the application. The root factory - returns the traversal root of an application. It is - conventionally named ``get_root``. An application may supply a - root factory to :app:`Pyramid` during the construction of a - :term:`Configurator`. If a root factory is not supplied, the - application uses a default root object. Use of the default root - object is useful in application which use :term:`URL dispatch` for - all URL-to-view code mappings. + The "root factory" of a :app:`Pyramid` application is called on every + request sent to the application. The root factory returns the traversal + root of an application. It is conventionally named ``get_root``. An + application may supply a root factory to :app:`Pyramid` during the + construction of a :term:`Configurator`. If a root factory is not + supplied, the application creates a default root object using the + :term:`default root factory`. + + default root factory + If an application does not register a :term:`root factory` at Pyramid + configuration time, a *default* root factory is used to created the + default root object. Use of the default root object is useful in + application which use :term:`URL dispatch` for all URL-to-view code + mappings, and does not (knowingly) use traversal otherwise. SQLAlchemy `SQLAlchemy <http://www.sqlalchemy.org/>`_ is an object @@ -506,13 +554,15 @@ Glossary `JavaScript Object Notation <http://www.json.org/>`_ is a data serialization format. + jQuery + A popular `Javascript library <http://jquery.org>`_. + renderer - A serializer that can be referred to via :term:`view - configuration` which converts a non-:term:`Response` return - values from a :term:`view` into a string (and ultimately a - response). Using a renderer can make writing views that require - templating or other serialization less tedious. See - :ref:`views_which_use_a_renderer` for more information. + A serializer which converts non-:term:`Response` return values from a + :term:`view` into a string, and ultimately into a response, usually + through :term:`view configuration`. Using a renderer can make writing + views that require templating or other serialization, like JSON, less + tedious. See :ref:`views_which_use_a_renderer` for more information. renderer factory A factory which creates a :term:`renderer`. See @@ -555,17 +605,21 @@ Glossary A wrapper around a Python function or class which accepts the function or class as its first argument and which returns an arbitrary object. :app:`Pyramid` provides several decorators, - used for configuration and return value modification purposes. See - also `PEP 318 <http://www.python.org/dev/peps/pep-0318/>`_. + used for configuration and return value modification purposes. + + .. seealso:: + + See also `PEP 318 <http://www.python.org/dev/peps/pep-0318/>`_. configuration declaration - An individual method call made to an instance of a :app:`Pyramid` - :term:`Configurator` object which performs an arbitrary action, such as - registering a :term:`view configuration` (via the ``add_view`` method of - the configurator) or :term:`route configuration` (via the ``add_route`` - method of the configurator). A set of configuration declarations is - also implied by the :term:`configuration decoration` detected by a - :term:`scan` of code in a package. + An individual method call made to a :term:`configuration directive`, + such as registering a :term:`view configuration` (via the + :meth:`~pyramid.config.Configurator.add_view` method of the + configurator) or :term:`route configuration` (via the + :meth:`~pyramid.config.Configurator.add_route` method of the + configurator). A set of configuration declarations is also implied by + the :term:`configuration decoration` detected by a :term:`scan` of code + in a package. configuration decoration Metadata implying one or more :term:`configuration declaration` @@ -581,7 +635,7 @@ Glossary configurator An object used to do :term:`configuration declaration` within an application. The most common configurator is an instance of the - ``pyramid.config.Configurator`` class. + :class:`pyramid.config.Configurator` class. imperative configuration The configuration mode in which you use Python to call methods on @@ -589,28 +643,27 @@ Glossary declaration` required by your application. declarative configuration - The configuration mode in which you use :term:`ZCML` to make a set of - :term:`configuration declaration` statements. See :term:`pyramid_zcml`. - - Not Found view - An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a ``pyramid.exceptions.NotFound`` - exception from within :term:`view` code or :term:`root factory` - code, or when the current request doesn't match any :term:`view - configuration`. :app:`Pyramid` provides a default - implementation of a not found view; it can be overridden. See + The configuration mode in which you use the combination of + :term:`configuration decoration` and a :term:`scan` to configure your + Pyramid application. + + Not Found View + An :term:`exception view` invoked by :app:`Pyramid` when the developer + explicitly raises a :class:`pyramid.httpexceptions.HTTPNotFound` + exception from within :term:`view` code or :term:`root factory` code, + or when the current request doesn't match any :term:`view + configuration`. :app:`Pyramid` provides a default implementation of a + Not Found View; it can be overridden. See :ref:`changing_the_notfound_view`. Forbidden view - An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a - ``pyramid.exceptions.Forbidden`` exception from within - :term:`view` code or :term:`root factory` code, or when the - :term:`view configuration` and :term:`authorization policy` + An :term:`exception view` invoked by :app:`Pyramid` when the developer + explicitly raises a :class:`pyramid.httpexceptions.HTTPForbidden` + exception from within :term:`view` code or :term:`root factory` code, + or when the :term:`view configuration` and :term:`authorization policy` found for a request disallows a particular view invocation. - :app:`Pyramid` provides a default implementation of a - forbidden view; it can be overridden. See - :ref:`changing_the_forbidden_view`. + :app:`Pyramid` provides a default implementation of a forbidden view; + it can be overridden. See :ref:`changing_the_forbidden_view`. Exception view An exception view is a :term:`view callable` which may be @@ -618,16 +671,27 @@ Glossary request processing. See :ref:`exception_views` for more information. + HTTP Exception + The set of exception classes defined in :mod:`pyramid.httpexceptions`. + These can be used to generate responses with various status codes when + raised or returned from a :term:`view callable`. + + .. seealso:: + + See also :ref:`http_exceptions`. + thread local A thread-local variable is one which is essentially a global variable in terms of how it is accessed and treated, however, each `thread <http://en.wikipedia.org/wiki/Thread_(computer_science)>`_ used by the application may have a different value for this same "global" variable. :app:`Pyramid` uses a small number of thread local variables, as - described in :ref:`threadlocals_chapter`. See also the `threading.local - documentation - <http://docs.python.org/library/threading.html#threading.local>`_ for - more information. + described in :ref:`threadlocals_chapter`. + + .. seealso:: + + See also the :class:`stdlib documentation <threading.local>` + for more information. multidict An ordered dictionary that can have multiple values for each key. Adds @@ -641,7 +705,11 @@ Glossary Agendaless Consulting A consulting organization formed by Paul Everitt, Tres Seaver, - and Chris McDonough. See also http://agendaless.com . + and Chris McDonough. + + .. seealso:: + + See also `Agendaless Consulting <http://agendaless.com>`_. Jython A `Python implementation <http://www.jython.org/>`_ written for @@ -655,11 +723,11 @@ Glossary The C implementation of the Python language. This is the reference implementation that most people refer to as simply "Python"; :term:`Jython`, Google's App Engine, and `PyPy - <http://codespeak.net/pypy/dist/pypy/doc/>`_ are examples of + <http://doc.pypy.org/en/latest/>`_ are examples of non-C based Python implementations. View Lookup - The act of finding and invoking the "best" :term:`view callable` + The act of finding and invoking the "best" :term:`view callable`, given a :term:`request` and a :term:`context` resource. Resource Location @@ -673,7 +741,7 @@ Glossary :app:`Pyramid` runs on GAE. Venusian - `Venusian <http://docs.repoze.org/venusian>`_ is a library which + :ref:`Venusian` is a library which allows framework authors to defer decorator actions. Instead of taking actions when a function (or class) decorator is executed at import time, the action usually taken by the decorator is @@ -694,15 +762,21 @@ Glossary made. For example the word "java" might be translated differently if the translation domain is "programming-languages" than would be if the translation domain was "coffee". A - translation domain is represnted by a collection of ``.mo`` files + translation domain is represented by a collection of ``.mo`` files within one or more :term:`translation directory` directories. + Translation Context + A string representing the "context" in which a translation was + made within a given :term:`translation domain`. See the gettext + documentation, `11.2.5 Using contexts for solving ambiguities + <https://www.gnu.org/software/gettext/manual/gettext.html#Contexts>`_ + for more information. + Translator - A callable which receives a :term:`translation string` and - returns a translated Unicode object for the purposes of - internationalization. A :term:`localizer` supplies a - translator to a :app:`Pyramid` application accessible via its - ``translate`` method. + A callable which receives a :term:`translation string` and returns a + translated Unicode object for the purposes of internationalization. A + :term:`localizer` supplies a translator to a :app:`Pyramid` application + accessible via its :class:`~pyramid.i18n.Localizer.translate` method. Translation Directory A translation directory is a :term:`gettext` translation @@ -740,15 +814,15 @@ Glossary library, used by the :app:`Pyramid` translation machinery. Babel - A `collection of tools <http://babel.edgewall.org/>`_ for - internationalizing Python applications. :app:`Pyramid` does - not depend on Babel to operate, but if Babel is installed, - additional locale functionality becomes available to your - application. + A `collection of tools <http://babel.pocoo.org/en/latest/>`_ for + internationalizing Python applications. :app:`Pyramid` does not depend on + Babel to operate, but if Babel is installed, additional locale + functionality becomes available to your application. Lingua - A package by Wichert Akkerman which provides :term:`Babel` message - extractors for Python source files and Chameleon ZPT template files. + A package by Wichert Akkerman which provides the ``pot-create`` + command to extract translateable messages from Python sources + and Chameleon ZPT template files. Message Identifier A string used as a translation lookup key during localization. @@ -763,25 +837,33 @@ Glossary The act of creating software with a user interface that can potentially be displayed in more than one language or cultural context. Often shortened to "i18n" (because the word - "internationalization" is I, 18 letters, then N). See also: - :term:`Localization`. + "internationalization" is I, 18 letters, then N). + + .. seealso:: + + See also :term:`Localization`. Localization The process of displaying the user interface of an internationalized application in a particular language or cultural context. Often shortened to "l10" (because the word - "localization" is L, 10 letters, then N). See also: - :term:`Internationalization`. + "localization" is L, 10 letters, then N). + + .. seealso:: + + See also :term:`Internationalization`. renderer globals - Values injected as names into a renderer based on application - policy. See :ref:`adding_renderer_globals` for more - information. + Values injected as names into a renderer by a + :class:`pyramid.event.BeforeRender` event. response callback A user-defined callback executed by the :term:`router` at a point after a :term:`response` object is successfully created. - See :ref:`using_response_callbacks`. + + .. seealso:: + + See also :ref:`using_response_callbacks`. finished callback A user-defined callback executed by the :term:`router` @@ -790,12 +872,12 @@ Glossary pregenerator A pregenerator is a function associated by a developer with a - :term:`route`. It is called by :func:`pyramid.url.route_url` - in order to adjust the set of arguments passed to it by the user - for special purposes. It will influence the URL returned by - ``route_url``. See - :class:`pyramid.interfaces.IRoutePregenerator` for more - information. + :term:`route`. It is called by + :meth:`~pyramid.request.Request.route_url` in order to adjust the set + of arguments passed to it by the user for special purposes. It will + influence the URL returned by + :meth:`~pyramid.request.Request.route_url`. See + :class:`pyramid.interfaces.IRoutePregenerator` for more information. session A namespace that is valid for some period of continual activity @@ -803,12 +885,15 @@ Glossary application. session factory - A callable, which, when called with a single argument named - ``request`` (a :term:`request` object), returns a - :term:`session` object. + A callable, which, when called with a single argument named ``request`` + (a :term:`request` object), returns a :term:`session` object. See + :ref:`using_the_default_session_factory`, + :ref:`using_alternate_session_factories` and + :meth:`pyramid.config.Configurator.set_session_factory` for more + information. Mako - `Mako <http://www.makotemplates.org/>`_ is a template language language + `Mako <http://www.makotemplates.org/>`_ is a template language which refines the familiar ideas of componentized layout and inheritance using Python with Python scoping and calling semantics. @@ -823,18 +908,14 @@ Glossary Deployment settings Deployment settings are settings passed to the :term:`Configurator` as a ``settings`` argument. These are later accessible via a - ``request.registry.settings`` dictionary. Deployment settings can be - used as global application values. + ``request.registry.settings`` dictionary in views or as + ``config.registry.settings`` in configuration code. Deployment settings + can be used as global application values. WebTest `WebTest <http://pythonpaste.org/webtest/>`_ is a package which can help you write functional tests for your WSGI application. - WebError - WSGI middleware which can display debuggable traceback information in - the browser when an exception is raised by a Pyramid application. See - http://pypi.python.org/pypi/WebError . - view mapper A view mapper is a class which implements the :class:`pyramid.interfaces.IViewMapperFactory` interface, which performs @@ -849,21 +930,15 @@ Glossary pyramid_zcml An add-on package to :app:`Pyramid` which allows applications to be - configured via ZCML. It is available on :term:`PyPI`. If you use - ``pyramid_zcml``, you can use ZCML as an alternative to - :term:`imperative configuration`. + configured via :term:`ZCML`. It is available on :term:`PyPI`. If you + use :mod:`pyramid_zcml`, you can use ZCML as an alternative to + :term:`imperative configuration` or :term:`configuration decoration`. ZCML `Zope Configuration Markup Language <http://www.muthukadan.net/docs/zca.html#zcml>`_, an XML dialect used by Zope and :term:`pyramid_zcml` for configuration tasks. - ZCML directive - A ZCML "tag" such as ``<view>`` or ``<route>``. - - ZCML declaration - The concrete use of a :term:`ZCML directive` within a ZCML file. - pyramid_handlers An add-on package which allows :app:`Pyramid` users to create classes that are analogues of Pylons 1 "controllers". See @@ -877,22 +952,187 @@ Glossary on the Jinja2 templating system. Akhet - Akhet is a Pyramid-based development environment which provides a - Pylons-esque scaffold which sports support for :term:`view handler` - application development, :term:`SQLAlchemy` support, :term:`Mako` - templating by default, and other Pylons-like features. See - http://docs.pylonsproject.org/projects/akhet/dev/index.html for more - information. + `Akhet <http://docs.pylonsproject.org/projects/akhet/en/latest/>`_ is a + Pyramid library and demo application with a Pylons-like feel. + It's most known for its former application scaffold, which helped + users transition from Pylons and those preferring a more Pylons-like API. + The scaffold has been retired but the demo plays a similar role. - Pyramid Cookbook - An additional documentation resource for Pyramid which presents topical, - practical usages of Pyramid available via - http://docs.pylonsproject.org/ . + Pyramid Community Cookbook + Additional, community-based documentation for Pyramid which presents + topical, practical uses of Pyramid: + :ref:`Pyramid Community Cookbook <cookbook:pyramid-cookbook>` distutils The standard system for packaging and distributing Python packages. See http://docs.python.org/distutils/index.html for more information. :term:`setuptools` is actually an *extension* of the Distutils. + exception response + A :term:`response` that is generated as the result of a raised exception + being caught by an :term:`exception view`. + + PyPy + PyPy is an "alternative implementation of the Python + language": http://pypy.org/ + + tween + A bit of code that sits between the Pyramid router's main request + handling function and the upstream WSGI component that uses + :app:`Pyramid` as its 'app'. The word "tween" is a contraction of + "between". A tween may be used by Pyramid framework extensions, to + provide, for example, Pyramid-specific view timing support, bookkeeping + code that examines exceptions before they are returned to the upstream + WSGI application, or a variety of other features. Tweens behave a bit + like :term:`WSGI` :term:`middleware` but they have the benefit of running in a + context in which they have access to the Pyramid :term:`application + registry` as well as the Pyramid rendering machinery. See + :ref:`registering_tweens`. + + pyramid_debugtoolbar + A Pyramid add-on which displays a helpful debug toolbar "on top of" HTML + pages rendered by your application, displaying request, routing, and + database information. :mod:`pyramid_debugtoolbar` is configured into + the ``development.ini`` of all applications which use a Pyramid + :term:`scaffold`. For more information, see + http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/. + + scaffold + A project template that generates some of the major parts of a Pyramid + application and helps users to quickly get started writing larger + applications. Scaffolds are usually used via the ``pcreate`` command. + + pyramid_exclog + A package which logs Pyramid application exception (error) information + to a standard Python logger. This add-on is most useful when + used in production applications, because the logger can be configured to + log to a file, to UNIX syslog, to the Windows Event Log, or even to + email. See its `documentation + <http://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. + + console script + A script written to the ``bin`` (on UNIX, or ``Scripts`` on Windows) + directory of a Python installation or :term:`virtual environment` as the + result of running ``pip install`` or ``pip install -e .``. + + introspector + An object with the methods described by + :class:`pyramid.interfaces.IIntrospector` that is available in both + configuration code (for registration) and at runtime (for querying) that + allows a developer to introspect configuration statements and + relationships between those statements. + + conflict resolution + Pyramid attempts to resolve ambiguous configuration statements made by + application developers via automatic conflict resolution. Automatic + conflict resolution is described in + :ref:`automatic_conflict_resolution`. If Pyramid cannot resolve + ambiguous configuration statements, it is possible to manually resolve + them as described in :ref:`manually_resolving_conflicts`. + + configuration directive + A method of the :term:`Configurator` which causes a configuration action + to occur. The method :meth:`pyramid.config.Configurator.add_view` is a + configuration directive, and application developers can add their own + directives as necessary (see :ref:`add_directive`). + + action + Represents a pending configuration statement generated by a call to a + :term:`configuration directive`. The set of pending configuration + actions are processed when :meth:`pyramid.config.Configurator.commit` is + called. + + discriminator + The unique identifier of an :term:`action`. + + introspectable + An object which implements the attributes and methods described in + :class:`pyramid.interfaces.IIntrospectable`. Introspectables are used + by the :term:`introspector` to display configuration information about + a running Pyramid application. An introspectable is associated with a + :term:`action` by virtue of the + :meth:`pyramid.config.Configurator.action` method. + + asset descriptor + An instance representing an :term:`asset specification` provided by the + :meth:`pyramid.path.AssetResolver.resolve` method. It supports the + methods and attributes documented in + :class:`pyramid.interfaces.IAssetDescriptor`. + + Waitress + A :term:`WSGI` server that runs on UNIX and Windows under Python 2.6+ + and Python 3.2+. Projects generated via Pyramid scaffolding use + Waitress as a WGSI server. See + http://docs.pylonsproject.org/projects/waitress/en/latest/ for detailed + information. + + Green Unicorn + Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under + Python 2.6+ or Python 3.1+. See http://gunicorn.org/ for detailed + information. + predicate factory + A callable which is used by a third party during the registration of a + route, view, or subscriber predicates to extend the configuration + system. See :ref:`registering_thirdparty_predicates` for more + information. + add-on + A Python :term:`distribution` that uses Pyramid's extensibility + to plug into a Pyramid application and provide extra, + configurable services. + + pyramid_redis_sessions + A package by Eric Rasmussen which allows you to store Pyramid session + data in a Redis database. See + https://pypi.python.org/pypi/pyramid_redis_sessions for more information. + + cache busting + A technique used when serving a cacheable static asset in order to force + a client to query the new version of the asset. See :ref:`cache_busting` + for more information. + + view deriver + A view deriver is a composable component of the view pipeline which is + used to create a :term:`view callable`. A view deriver is a callable + implementing the :class:`pyramid.interfaces.IViewDeriver` interface. + Examples of built-in derivers including view mapper, the permission + checker, and applying a renderer to a dictionary returned from the view. + + truthy string + A string represeting a value of ``True``. Acceptable values are + ``t``, ``true``, ``y``, ``yes``, ``on`` and ``1``. + + falsey string + A string represeting a value of ``False``. Acceptable values are + ``f``, ``false``, ``n``, ``no``, ``off`` and ``0``. + + pip + The :term:`Python Packaging Authority`'s recommended tool for installing + Python packages. + + pyvenv + The :term:`Python Packaging Authority` formerly recommended using the + ``pyvenv`` command for `creating virtual environments on Python 3.4 and + 3.5 + <https://packaging.python.org/en/latest/installing/#creating-virtual-environments>`_, + but it was deprecated in 3.6 in favor of ``python3 -m venv`` on UNIX or + ``python -m venv`` on Windows, which is backward compatible on Python + 3.3 and greater. + + virtual environment + An isolated Python environment that allows packages to be installed for + use by a particular application, rather than being installed system wide. + + venv + The :term:`Python Packaging Authority`'s recommended tool for creating + virtual environments on Python 3.3 and greater. + + Note: whenever you encounter commands prefixed with ``$VENV`` (Unix) + or ``%VENV`` (Windows), know that that is the environment variable whose + value is the root of the virtual environment in question. + + Python Packaging Authority + The `Python Packaging Authority (PyPA) <https://www.pypa.io/en/latest/>`_ + is a working group that maintains many of the relevant projects in Python + packaging.
\ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a4743af9b..aecc26d2e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,199 +1,223 @@ .. _index: -================================================= -The Pyramid Web Application Development Framework -================================================= +========================= +The Pyramid Web Framework +========================= -:app:`Pyramid` is a small, fast, down-to-earth Python web application -development framework. It is developed as part of the `Pylons Project -<http://docs.pylonsproject.org/>`_. It is licensed under a `BSD-like license -<http://repoze.org/license.html>`_. +:app:`Pyramid` is a small, fast, down-to-earth Python web framework. It is +developed as part of the `Pylons Project <http://docs.pylonsproject.org/>`_. +It is licensed under a `BSD-like license <http://repoze.org/license.html>`_. -Front Matter -============ +Here is one of the simplest :app:`Pyramid` applications you can make: + +.. literalinclude:: narr/helloworld.py + +After you install :app:`Pyramid` and run this application, when you visit +`<http://localhost:8080/hello/world>`_ in a browser, you will see the text +``Hello, world!`` See :ref:`firstapp_chapter` for a full explanation of how +this application works. + + +.. _html_getting_started: + +Getting Started +=============== + +If you are new to Pyramid, we have a few resources that can help you get up to +speed right away. .. toctree:: - :maxdepth: 1 + :hidden: + + quick_tour + quick_tutorial/index + +* :doc:`quick_tour` gives an overview of the major features in Pyramid, + covering a little about a lot. + +* :doc:`quick_tutorial/index` is similar to the Quick Tour, but in a tutorial + format, with somewhat deeper treatment of each topic and with working code. + +* Like learning by example? Visit the official :ref:`html_tutorials` as well as + the community-contributed :ref:`Pyramid Tutorials + <tutorials:pyramid-tutorials>` and :ref:`Pyramid Community Cookbook + <cookbook:pyramid-cookbook>`. + +* For help getting Pyramid set up, try :ref:`installing_chapter`. + +* Need help? See :ref:`Support and Development <support-and-development>`. + + +.. _html_tutorials: - copyright.rst - conventions.rst +Tutorials +========= -"What's New" Documents -====================== +Official tutorials explaining how to use :app:`Pyramid` to build various types +of applications, and how to deploy :app:`Pyramid` applications to various +platforms. .. toctree:: :maxdepth: 1 - whatsnew-1.0 - whatsnew-1.1 + tutorials/wiki2/index.rst + tutorials/wiki/index.rst + tutorials/modwsgi/index.rst + -Narrative documentation +.. _support-and-development: + +Support and Development ======================= -Narrative documentation in chapter form explaining how to use -:app:`Pyramid`. +The `Pylons Project web site <http://pylonsproject.org/>`_ is the main online +source of :app:`Pyramid` support and development information. + +To report bugs, use the `issue tracker +<https://github.com/Pylons/pyramid/issues>`_. + +If you've got questions that aren't answered by this documentation, contact the +`Pylons-discuss maillist <http://groups.google.com/group/pylons-discuss>`_ or +join the `#pyramid IRC channel <irc://irc.freenode.net/#pyramid>`_. + +Browse and check out tagged and trunk versions of :app:`Pyramid` via the +`Pyramid GitHub repository <https://github.com/Pylons/pyramid/>`_. To check out +the trunk via ``git``, use either command: + +.. code-block:: text + + # If you have SSH keys configured on GitHub: + git clone git@github.com:Pylons/pyramid.git + + # Otherwise, HTTPS will work, using your GitHub login: + git clone https://github.com/Pylons/pyramid.git + +To find out how to become a contributor to :app:`Pyramid`, please see the +`contributor's section of the documentation +<http://docs.pylonsproject.org/en/latest/#contributing>`_. + + +.. _html_narrative_documentation: + +Narrative Documentation +======================= + +Narrative documentation in chapter form explaining how to use :app:`Pyramid`. .. toctree:: :maxdepth: 2 narr/introduction narr/install - narr/configuration narr/firstapp + narr/configuration narr/project narr/startup + narr/router narr/urldispatch - narr/muchadoabouttraversal - narr/traversal narr/views narr/renderers narr/templates narr/viewconfig - narr/resources narr/assets narr/webob narr/sessions - narr/security - narr/hybrid - narr/i18n - narr/vhosting narr/events narr/environment + narr/logging + narr/paste + narr/commandline + narr/i18n + narr/vhosting narr/testing + narr/resources + narr/hellotraversal + narr/muchadoabouttraversal + narr/traversal + narr/security + narr/hybrid + narr/subrequest narr/hooks - narr/advconfig + narr/introspector narr/extending - narr/router + narr/advconfig + narr/extconfig + narr/scaffolding + narr/upgrading narr/threadlocals narr/zca -Tutorials -========= -Detailed tutorials explaining how to use :app:`Pyramid` to build -various types of applications and how to deploy :app:`Pyramid` -applications to various platforms. +API Documentation +================= + +Comprehensive reference material for every public API exposed by +:app:`Pyramid`: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :glob: - tutorials/wiki2/index.rst - tutorials/wiki/index.rst - tutorials/bfg/index.rst - tutorials/gae/index.rst - tutorials/modwsgi/index.rst + api/index + api/* -Reference Material -================== -Reference material includes documentation for every :app:`Pyramid` API. +``p*`` Scripts Documentation +============================ + +``p*`` scripts included with :app:`Pyramid`:. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :glob: - api + pscripts/index + pscripts/* -Detailed Change History -======================= + +Change History +============== .. toctree:: :maxdepth: 1 + whatsnew-1.7 + whatsnew-1.6 + whatsnew-1.5 + whatsnew-1.4 + whatsnew-1.3 + whatsnew-1.2 + whatsnew-1.1 + whatsnew-1.0 changes -Design Documentation -==================== + +Design Documents +================ .. toctree:: :maxdepth: 1 designdefense -Sample Applications -=================== -`cluegun <https://github.com/Pylons/cluegun>`_ is a simple pastebin -application based on Rocky Burt's `ClueBin -<http://pypi.python.org/pypi/ClueBin/0.2.3>`_. It demonstrates form -processing, security, and the use of :term:`ZODB` within a :app:`Pyramid` -application. Check this application out via: - -.. code-block:: text +Copyright, Trademarks, and Attributions +======================================= - git clone git://github.com/Pylons/cluegun.git - -`virginia <https://github.com/Pylons/virginia>`_ is a very simple dynamic -file rendering application. It is willing to render structured text -documents, HTML documents, and images from a filesystem directory. An -earlier version of this application runs the `repoze.org -<http://repoze.org>`_ website. Check this application out via: - -.. code-block:: text - - git clone git://github.com/Pylons/virginia.git - -`shootout <https://github.com/Pylons/shootout>`_ is an example "idea -competition" application by Carlos de la Guardia and Lukasz Fidosz. It -demonstrates :term:`URL dispatch`, simple authentication, integration -with `SQLAlchemy <http://www.sqlalchemy.org/>`_ and ``pyramid_simpleform``. -Check this application out of version control via: - -.. code-block:: text - - git clone git://github.com/Pylons/shootout.git - -Older Sample Applications (repoze.bfg) -====================================== - -.. note:: - - These applications are for an older version of :app:`Pyramid`, which was - named :mod:`repoze.bfg`. They won't work unmodified under Pyramid, but - might provide useful clues. - -`bfgsite <http://svn.repoze.org/bfgsite/trunk>`_ is the software which -runs the `bfg.repoze.org <http://bfg.repoze.org>`_ website. It -demonstrates integration with Trac, and includes several -mini-applications such as a pastebin and tutorial engine. Check a -buildout for this application out of Subversion via: - -.. code-block:: text - - svn co http://svn.repoze.org/buildouts/bfgsite/ bfgsite_buildout - -`KARL <http://karlproject.org>`_ is a moderately-sized application -(roughly 70K lines of Python code) built on top of :mod:`repoze.bfg` -and other Repoze software. It is an open source web system for -collaboration, organizational intranets, and knowledge management, It -provides facilities for wikis, calendars, manuals, searching, tagging, -commenting, and file uploads. See the `KARL site -<http://karlproject.org>`_ for download and installation details. - -Support and Development -======================= - -The `Pylons Project web site <http://pylonsproject.org/>`_ is the main online -source of :app:`Pyramid` support and development information. +.. toctree:: + :maxdepth: 1 -To report bugs, use the `issue tracker -<http://github.com/Pylons/pyramid/issues>`_. + copyright -If you've got questions that aren't answered by this documentation, -contact the `Pylons-devel maillist -<http://groups.google.com/group/pylons-devel>`_ or join the `#pylons -IRC channel <irc://irc.freenode.net/#pylons>`_. -Browse and check out tagged and trunk versions of :app:`Pyramid` via -the `Pyramid GitHub repository <http://github.com/Pylons/pyramid/>`_. -To check out the trunk via ``git``, use this command: +Typographical Conventions +========================= -.. code-block:: text +.. toctree:: + :maxdepth: 1 - git clone git@github.com:Pylons/pyramid.git + conventions -To find out how to become a contributor to :app:`Pyramid`, please see the -`contributor's section of the documentation -<http://docs.pylonsproject.org/index.html#contributing>`_. Index and Glossary ================== @@ -201,3 +225,10 @@ Index and Glossary * :ref:`glossary` * :ref:`genindex` * :ref:`search` + + +.. toctree:: + :hidden: + + glossary + diff --git a/docs/latexindex.rst b/docs/latexindex.rst index a4926bf30..c4afff212 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -1,8 +1,10 @@ +:orphan: + .. _latexindex: -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -The :app:`Pyramid` Web Application Framework -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +========================= +The Pyramid Web Framework +========================= .. frontmatter:: @@ -28,31 +30,41 @@ Narrative Documentation narr/introduction narr/install - narr/configuration narr/firstapp + narr/configuration narr/project + narr/startup + narr/router narr/urldispatch - narr/muchadoabouttraversal - narr/traversal narr/views narr/renderers narr/templates narr/viewconfig - narr/resources narr/assets narr/webob narr/sessions - narr/security - narr/hybrid - narr/i18n - narr/vhosting narr/events narr/environment + narr/logging + narr/paste + narr/commandline + narr/i18n + narr/vhosting narr/testing + narr/resources + narr/hellotraversal + narr/muchadoabouttraversal + narr/traversal + narr/security + narr/hybrid + narr/subrequest narr/hooks - narr/advconfig + narr/introspector narr/extending - narr/startup + narr/advconfig + narr/extconfig + narr/scaffolding + narr/upgrading narr/threadlocals narr/zca @@ -64,45 +76,20 @@ Tutorials .. toctree:: :maxdepth: 1 - tutorials/wiki/index.rst tutorials/wiki2/index.rst - tutorials/bfg/index.rst - tutorials/gae/index.rst + tutorials/wiki/index.rst tutorials/modwsgi/index.rst -.. _api_reference: +.. _api_documentation: -API Reference -@@@@@@@@@@@@@ +API Documentation +@@@@@@@@@@@@@@@@@ .. toctree:: :maxdepth: 1 + :glob: - api/authorization - api/authentication - api/chameleon_text - api/chameleon_zpt - api/config - api/events - api/exceptions - api/httpexceptions - api/i18n - api/interfaces - api/location - api/paster - api/registry - api/renderers - api/request - api/response - api/scripting - api/security - api/settings - api/testing - api/threadlocal - api/traversal - api/url - api/view - api/wsgi + api/* .. backmatter:: diff --git a/docs/make_book b/docs/make_book index dc8381845..94e249441 100755 --- a/docs/make_book +++ b/docs/make_book @@ -1,4 +1,4 @@ #!/bin/sh -make clean latex SPHINXBUILD=../bookenv/bin/sphinx-build BOOK=1 +make clean latex SPHINXBUILD=../env/bin/sphinx-build BOOK=1 cd _build/latex && make all-pdf diff --git a/docs/make_epub b/docs/make_epub new file mode 100755 index 000000000..cf9263451 --- /dev/null +++ b/docs/make_epub @@ -0,0 +1,2 @@ +#!/bin/sh +make clean epub SPHINXBUILD=../env/bin/sphinx-build diff --git a/docs/make_pdf b/docs/make_pdf new file mode 100755 index 000000000..6c9863bc9 --- /dev/null +++ b/docs/make_pdf @@ -0,0 +1,4 @@ +#!/bin/sh +make clean latex SPHINXBUILD=../env/bin/sphinx-build +cd _build/latex && make all-pdf + diff --git a/docs/narr/MyProject/README.txt b/docs/narr/MyProject/README.txt index 5e10949fc..70759eba1 100644 --- a/docs/narr/MyProject/README.txt +++ b/docs/narr/MyProject/README.txt @@ -1,4 +1,12 @@ MyProject README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini diff --git a/docs/narr/MyProject/development.ini b/docs/narr/MyProject/development.ini index 29486ce56..94fece8ce 100644 --- a/docs/narr/MyProject/development.ini +++ b/docs/narr/MyProject/development.ini @@ -1,23 +1,36 @@ -[app:MyProject] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:MyProject -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en - -[pipeline:main] -pipeline = - egg:WebError#evalerror - MyProject + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, myproject @@ -44,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/narr/MyProject/myproject/__init__.py b/docs/narr/MyProject/myproject/__init__.py index 04e219e36..ad5ecbc6f 100644 --- a/docs/narr/MyProject/myproject/__init__.py +++ b/docs/narr/MyProject/myproject/__init__.py @@ -1,12 +1,12 @@ from pyramid.config import Configurator -from myproject.resources import Root + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - config = Configurator(root_factory=Root, settings=settings) - config.add_view('myproject.views.my_view', - context='myproject.resources.Root', - renderer='myproject:templates/mytemplate.pt') - config.add_static_view('static', 'myproject:static') + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() return config.make_wsgi_app() diff --git a/docs/narr/MyProject/myproject/resources.py b/docs/narr/MyProject/myproject/resources.py deleted file mode 100644 index 3d811895c..000000000 --- a/docs/narr/MyProject/myproject/resources.py +++ /dev/null @@ -1,3 +0,0 @@ -class Root(object): - def __init__(self, request): - self.request = request diff --git a/docs/narr/MyProject/myproject/static/pylons.css b/docs/narr/MyProject/myproject/static/pylons.css deleted file mode 100644 index 33b21ac1a..000000000 --- a/docs/narr/MyProject/myproject/static/pylons.css +++ /dev/null @@ -1,64 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#bottom{width:100%;} -#top{color:#000000;height:230px; -background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/narr/MyProject/myproject/static/pyramid-16x16.png b/docs/narr/MyProject/myproject/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/narr/MyProject/myproject/static/pyramid-16x16.png diff --git a/docs/narr/MyProject/myproject/static/pyramid.png b/docs/narr/MyProject/myproject/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/narr/MyProject/myproject/static/pyramid.png +++ b/docs/narr/MyProject/myproject/static/pyramid.png diff --git a/docs/narr/MyProject/myproject/static/theme.css b/docs/narr/MyProject/myproject/static/theme.css new file mode 100644 index 000000000..be50ad420 --- /dev/null +++ b/docs/narr/MyProject/myproject/static/theme.css @@ -0,0 +1,152 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/narr/MyProject/myproject/templates/mytemplate.pt b/docs/narr/MyProject/myproject/templates/mytemplate.pt index 632c34876..543663fe8 100644 --- a/docs/narr/MyProject/myproject/templates/mytemplate.pt +++ b/docs/narr/MyProject/myproject/templates/mytemplate.pt @@ -1,106 +1,67 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" - xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('myproject:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('myproject:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" - href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('myproject:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div> - <img src="${request.static_url('myproject:static/pyramid.png')}" - width="750" height="169" alt="pyramid"/> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('myproject:static/pyramid-16x16.png')}"> + + <title>Starter Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('myproject:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('myproject:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, - an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" - action="http://docs.pylonsproject.org/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org"> - Pylons Website - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation"> - Narrative Documentation - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation"> - API Documentation - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials"> - Tutorials - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history"> - Change History - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications"> - Sample Applications - </a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development"> - Support and Development - </a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid"> - IRC Channel - </a> - </li> - </ul> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2010, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py index 5fa710278..fd414cced 100644 --- a/docs/narr/MyProject/myproject/tests.py +++ b/docs/narr/MyProject/myproject/tests.py @@ -2,6 +2,7 @@ import unittest from pyramid import testing + class ViewTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -10,9 +11,19 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from myproject.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'MyProject') +class FunctionalTests(unittest.TestCase): + def setUp(self): + from myproject import main + app = main({}) + from webtest import TestApp + self.testapp = TestApp(app) + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'Pyramid' in res.body) diff --git a/docs/narr/MyProject/myproject/views.py b/docs/narr/MyProject/myproject/views.py index c43b34460..c383c5716 100644 --- a/docs/narr/MyProject/myproject/views.py +++ b/docs/narr/MyProject/myproject/views.py @@ -1,2 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): - return {'project':'MyProject'} + return {'project': 'MyProject'} diff --git a/docs/narr/MyProject/production.ini b/docs/narr/MyProject/production.ini index c1d0eee82..1174b1cc7 100644 --- a/docs/narr/MyProject/production.ini +++ b/docs/narr/MyProject/production.ini @@ -1,37 +1,30 @@ -[app:MyProject] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:MyProject -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en - -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = - -[pipeline:main] -pipeline = - weberror - MyProject + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, myproject @@ -43,11 +36,11 @@ keys = console keys = generic [logger_root] -level = INFO +level = WARN handlers = console [logger_myproject] -level = INFO +level = WARN handlers = qualname = myproject @@ -58,6 +51,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/narr/MyProject/setup.cfg b/docs/narr/MyProject/setup.cfg deleted file mode 100644 index 332e80a60..000000000 --- a/docs/narr/MyProject/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match = ^test -nocapture = 1 -cover-package = myproject -with-coverage = 1 -cover-erase = 1 - -[compile_catalog] -directory = myproject/locale -domain = MyProject -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = myproject/locale/MyProject.pot -width = 80 - -[init_catalog] -domain = MyProject -input_file = myproject/locale/MyProject.pot -output_dir = myproject/locale - -[update_catalog] -domain = MyProject -input_file = myproject/locale/MyProject.pot -output_dir = myproject/locale -previous = true diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py index a64d65ba6..a911eff6d 100644 --- a/docs/narr/MyProject/setup.py +++ b/docs/narr/MyProject/setup.py @@ -3,21 +3,34 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() -requires = ['pyramid', 'WebError'] +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='MyProject', version='0.0', description='MyProject', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -25,13 +38,12 @@ setup(name='MyProject', packages=find_packages(), include_package_data=True, zip_safe=False, + extras_require={ + 'testing': tests_require, + }, install_requires=requires, - tests_require=requires, - test_suite="myproject", - entry_points = """\ + entry_points="""\ [paste.app_factory] main = myproject:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 3bd9c2a4e..bdcdf45a4 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -6,15 +6,14 @@ Advanced Configuration ====================== -To support application extensibility, the :app:`Pyramid` -:term:`Configurator`, by default, detects configuration conflicts and allows -you to include configuration imperatively from other packages or modules. It -also, by default, performs configuration in two separate phases. This allows -you to ignore relative configuration statement ordering in some -circumstances. +To support application extensibility, the :app:`Pyramid` :term:`Configurator` +by default detects configuration conflicts and allows you to include +configuration imperatively from other packages or modules. It also by default +performs configuration in two separate phases. This allows you to ignore +relative configuration statement ordering in some circumstances. .. index:: - single: imperative configuration + pair: configuration; conflict detection .. _conflict_detection: @@ -27,7 +26,7 @@ configured imperatively: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -38,7 +37,8 @@ configured imperatively: config = Configurator() config.add_view(hello_world) app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() When you start this application, all will be OK. However, what happens if we try to add another view to the configuration with the same set of @@ -47,7 +47,7 @@ try to add another view to the configuration with the same set of .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -66,13 +66,14 @@ try to add another view to the configuration with the same set of config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() -The application now has two conflicting view configuration statements. When -we try to start it again, it won't start. Instead, we'll receive a traceback -that ends something like this: +The application now has two conflicting view configuration statements. When we +try to start it again, it won't start. Instead we'll receive a traceback that +ends something like this: -.. code-block:: guess +.. code-block:: text :linenos: Traceback (most recent call last): @@ -82,32 +83,29 @@ that ends something like this: self.commit() File "pyramid/pyramid/config.py", line 473, in commit self._ctx.execute_actions() - File "zope/configuration/config.py", line 600, in execute_actions - for action in resolveConflicts(self.actions): - File "zope/configuration/config.py", line 1507, in resolveConflicts - raise ConfigurationConflictError(conflicts) - zope.configuration.config.ConfigurationConflictError: + ... more code ... + pyramid.exceptions.ConfigurationConflictError: Conflicting configuration actions For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, None, None, None, None, None, False, None, None, None) - ('app.py', 14, '<module>', 'config.add_view(hello_world)') - ('app.py', 17, '<module>', 'config.add_view(hello_world)') + Line 14 of file app.py in <module>: 'config.add_view(hello_world)' + Line 17 of file app.py in <module>: 'config.add_view(goodbye_world)' This traceback is trying to tell us: -- We've got conflicting information for a set of view configuration - statements (The ``For:`` line). +- We've got conflicting information for a set of view configuration statements + (The ``For:`` line). - There are two statements which conflict, shown beneath the ``For:`` line: ``config.add_view(hello_world. 'hello')`` on line 14 of ``app.py``, and ``config.add_view(goodbye_world, 'hello')`` on line 17 of ``app.py``. -These two configuration statements are in conflict because we've tried to -tell the system that the set of :term:`predicate` values for both view +These two configuration statements are in conflict because we've tried to tell +the system that the set of :term:`predicate` values for both view configurations are exactly the same. Both the ``hello_world`` and ``goodbye_world`` views are configured to respond under the same set of -circumstances. This circumstance: the :term:`view name` (represented by the -``name=`` predicate) is ``hello``. +circumstances. This circumstance, the :term:`view name` represented by the +``name=`` predicate, is ``hello``. This presents an ambiguity that :app:`Pyramid` cannot resolve. Rather than allowing the circumstance to go unreported, by default Pyramid raises a @@ -118,12 +116,15 @@ Conflict detection happens for any kind of configuration: imperative configuration or configuration that results from the execution of a :term:`scan`. +.. _manually_resolving_conflicts: + Manually Resolving Conflicts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -There are a number of ways to manually resolve conflicts: the "right" way, by -strategically using :meth:`pyramid.config.Configurator.commit`, or by using -an "autocommitting" configurator. +There are a number of ways to manually resolve conflicts: by changing +registrations to not conflict, by strategically using +:meth:`pyramid.config.Configurator.commit`, or by using an "autocommitting" +configurator. The Right Thing +++++++++++++++ @@ -136,8 +137,7 @@ made by your application. Use the detail provided in the modify your configuration code accordingly. If you're getting a conflict while trying to extend an existing application, -and that application has a function which performs configuration like this -one: +and that application has a function which performs configuration like this one: .. code-block:: python :linenos: @@ -145,33 +145,37 @@ one: def add_routes(config): config.add_route(...) -Don't call this function directly with ``config`` as an argument. Instead, -use :meth:`pyramid.config.Configuration.include`: +Don't call this function directly with ``config`` as an argument. Instead, use +:meth:`pyramid.config.Configurator.include`: .. code-block:: python :linenos: config.include(add_routes) -Using :meth:`~pyramid.config.Configuration.include` instead of calling the -function directly provides a modicum of automated conflict resolution, with -the configuration statements you define in the calling code overriding those -of the included function. See also :ref:`automatic_conflict_resolution` and -:ref:`including_configuration`. +Using :meth:`~pyramid.config.Configurator.include` instead of calling the +function directly provides a modicum of automated conflict resolution, with the +configuration statements you define in the calling code overriding those of the +included function. + +.. seealso:: + + See also :ref:`automatic_conflict_resolution` and + :ref:`including_configuration`. Using ``config.commit()`` +++++++++++++++++++++++++ You can manually commit a configuration by using the -:meth:`~pyramid.config.Configurator.commit` method between configuration -calls. For example, we prevent conflicts from occurring in the application -we examined previously as the result of adding a ``commit``. Here's the -application that generates conflicts: +:meth:`~pyramid.config.Configurator.commit` method between configuration calls. +For example, we prevent conflicts from occurring in the application we examined +previously as the result of adding a ``commit``. Here's the application that +generates conflicts: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -190,15 +194,17 @@ application that generates conflicts: config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() -We can prevent the two ``add_view`` calls from conflicting by issuing a call -to :meth:`~pyramid.config.Configurator.commit` between them: +We can prevent the two ``add_view`` calls from conflicting by issuing a call to +:meth:`~pyramid.config.Configurator.commit` between them: .. code-block:: python :linenos: + :emphasize-lines: 16 - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -219,24 +225,24 @@ to :meth:`~pyramid.config.Configurator.commit` between them: config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() In the above example we've issued a call to -:meth:`~pyramid.config.Configurator.commit` between the two ``add_view`` -calls. :meth:`~pyramid.config.Configurator.commit` will cause any pending +:meth:`~pyramid.config.Configurator.commit` between the two ``add_view`` calls. +:meth:`~pyramid.config.Configurator.commit` will execute any pending configuration statements. Calling :meth:`~pyramid.config.Configurator.commit` is safe at any time. It -executes all pending configuration actions and leaves the configuration -action list "clean". +executes all pending configuration actions and leaves the configuration action +list "clean". -Note that :meth:`~pyramid.config.Configurator.commit` has no effect when -you're using an *autocommitting* configurator (see -:ref:`autocommitting_configurator`). +Note that :meth:`~pyramid.config.Configurator.commit` has no effect when you're +using an *autocommitting* configurator (see :ref:`autocommitting_configurator`). .. _autocommitting_configurator: -Using An Autocommitting Configurator +Using an Autocommitting Configurator ++++++++++++++++++++++++++++++++++++ You can also use a heavy hammer to circumvent conflict detection by using a @@ -270,17 +276,17 @@ Automatic Conflict Resolution If your code uses the :meth:`~pyramid.config.Configurator.include` method to include external configuration, some conflicts are automatically resolved. Configuration statements that are made as the result of an "include" will be -overridden by configuration statements that happen within the caller of -the "include" method. +overridden by configuration statements that happen within the caller of the +"include" method. -Automatic conflict resolution supports this goal: if a user wants to reuse a +Automatic conflict resolution supports this goal. If a user wants to reuse a Pyramid application, and they want to customize the configuration of this application without hacking its code "from outside", they can "include" a configuration function from the package and override only some of its configuration statements within the code that does the include. No conflicts -will be generated by configuration statements within the code which does the -including, even if configuration statements in the included code would -conflict if it was moved "up" to the calling code. +will be generated by configuration statements within the code that does the +including, even if configuration statements in the included code would conflict +if it was moved "up" to the calling code. Methods Which Provide Conflict Detection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -290,24 +296,36 @@ These are the methods of the configurator which provide conflict detection: :meth:`~pyramid.config.Configurator.add_view`, :meth:`~pyramid.config.Configurator.add_route`, :meth:`~pyramid.config.Configurator.add_renderer`, +:meth:`~pyramid.config.Configurator.add_request_method`, :meth:`~pyramid.config.Configurator.set_request_factory`, -:meth:`~pyramid.config.Configurator.set_renderer_globals_factory`, -:meth:`~pyramid.config.Configurator.set_locale_negotiator` and -:meth:`~pyramid.config.Configurator.set_default_permission`. +:meth:`~pyramid.config.Configurator.set_session_factory`, +:meth:`~pyramid.config.Configurator.set_request_property`, +:meth:`~pyramid.config.Configurator.set_root_factory`, +:meth:`~pyramid.config.Configurator.set_view_mapper`, +:meth:`~pyramid.config.Configurator.set_authentication_policy`, +:meth:`~pyramid.config.Configurator.set_authorization_policy`, +:meth:`~pyramid.config.Configurator.set_locale_negotiator`, +:meth:`~pyramid.config.Configurator.set_default_permission`, +:meth:`~pyramid.config.Configurator.add_traverser`, +:meth:`~pyramid.config.Configurator.add_resource_url_adapter`, +and :meth:`~pyramid.config.Configurator.add_response_adapter`. + +:meth:`~pyramid.config.Configurator.add_static_view` also indirectly provides +conflict detection, because it's implemented in terms of the conflict-aware +``add_route`` and ``add_view`` methods. -:meth:`~pyramid.config.Configurator.add_static_view` also indirectly -provides conflict detection, because it's implemented in terms of the -conflict-aware ``add_route`` and ``add_view`` methods. +.. index:: + pair: configuration; including from external sources .. _including_configuration: Including Configuration from External Sources --------------------------------------------- -Some application programmers will factor their configuration code in such a -way that it is easy to reuse and override configuration statements. For -example, such a developer might factor out a function used to add routes to -his application: +Some application programmers will factor their configuration code in such a way +that it is easy to reuse and override configuration statements. For example, +such a developer might factor out a function used to add routes to their +application: .. code-block:: python :linenos: @@ -315,8 +333,8 @@ his application: def add_routes(config): config.add_route(...) -Rather than calling this function directly with ``config`` as an argument. -Instead, use :meth:`pyramid.config.Configuration.include`: +Rather than calling this function directly with ``config`` as an argument, +instead use :meth:`pyramid.config.Configurator.include`: .. code-block:: python :linenos: @@ -326,7 +344,7 @@ Instead, use :meth:`pyramid.config.Configuration.include`: Using ``include`` rather than calling the function directly will allow :ref:`automatic_conflict_resolution` to work. -:meth:`~pyramid.config.Configuration.include` can also accept a :term:`module` +:meth:`~pyramid.config.Configurator.include` can also accept a :term:`module` as an argument: .. code-block:: python @@ -340,24 +358,24 @@ For this to work properly, the ``myapp`` module must contain a callable with the special name ``includeme``, which should perform configuration (like the ``add_routes`` callable we showed above as an example). -:meth:`~pyramid.config.Configuration.include` can also accept a :term:`dotted +:meth:`~pyramid.config.Configurator.include` can also accept a :term:`dotted Python name` to a function or a module. -.. note: See :ref:`the_include_tag` for a declarative alternative to - the :meth:`~pyramid.config.Configurator.include` method. +.. note:: See :ref:`the_include_tag` for a declarative alternative to the + :meth:`~pyramid.config.Configurator.include` method. .. _twophase_config: Two-Phase Configuration ----------------------- -When a non-autocommitting :term:`Configurator` is used to do configuration -(the default), configuration execution happens in two phases. In the first -phase, "eager" configuration actions (actions that must happen before all -others, such as registering a renderer) are executed, and *discriminators* -are computed for each of the actions that depend on the result of the eager -actions. In the second phase, the discriminators of all actions are compared -to do conflict detection. +When a non-autocommitting :term:`Configurator` is used to do configuration (the +default), configuration execution happens in two phases. In the first phase, +"eager" configuration actions (actions that must happen before all others, such +as registering a renderer) are executed, and *discriminators* are computed for +each of the actions that depend on the result of the eager actions. In the +second phase, the discriminators of all actions are compared to do conflict +detection. Due to this, for configuration methods that have no internal ordering constraints, execution order of configuration method calls is not important. @@ -381,15 +399,14 @@ Has the same result as: config.add_view('some.view', renderer='path_to_custom/renderer.rn') Even though the view statement depends on the registration of a custom -renderer, due to two-phase configuration, the order in which the -configuration statements are issued is not important. ``add_view`` will be -able to find the ``.rn`` renderer even if ``add_renderer`` is called after -``add_view``. +renderer, due to two-phase configuration, the order in which the configuration +statements are issued is not important. ``add_view`` will be able to find the +``.rn`` renderer even if ``add_renderer`` is called after ``add_view``. The same is untrue when you use an *autocommitting* configurator (see :ref:`autocommitting_configurator`). When an autocommitting configurator is -used, two-phase configuration is disabled, and configuration statements must -be ordered in dependency order. +used, two-phase configuration is disabled, and configuration statements must be +ordered in dependency order. Some configuration methods, such as :meth:`~pyramid.config.Configurator.add_route` have internal ordering @@ -397,72 +414,9 @@ constraints: the routes they imply require relative ordering. Such ordering constraints are not absolved by two-phase configuration. Routes are still added in configuration execution order. -.. _add_directive: - -Adding Methods to the Configurator via ``add_directive`` --------------------------------------------------------- - -Framework extension writers can add arbitrary methods to a -:term:`Configurator` by using the -:meth:`pyramid.config.Configurator.add_directive` method of the configurator. -This makes it possible to extend a Pyramid configurator in arbitrary ways, -and allows it to perform application-specific tasks more succinctly. - -The :meth:`~pyramid.config.Configurator.add_directive` method accepts two -positional arguments: a method name and a callable object. The callable -object is usually a function that takes the configurator instance as its -first argument and accepts other arbitrary positional and keyword arguments. -For example: - -.. code-block:: python - :linenos: - - from pyramid.events import NewRequest - from pyramid.config import Configurator - - def add_newrequest_subscriber(config, subscriber): - config.add_subscriber(subscriber, NewRequest). - - if __name__ == '__main__': - config = Configurator() - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can -then call the method by its given name as if it were a built-in method of the -Configurator: +More Information +---------------- -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - config.add_newrequest_subscriber(mysubscriber) - -A call to :meth:`~pyramid.config.Configurator.add_directive` is often -"hidden" within an ``includeme`` function within a "frameworky" package meant -to be included as per :ref:`including_configuration` via -:meth:`~pyramid.config.Configurator.include`. For example, if you put this -code in a package named ``pyramid_subscriberhelpers``: - -.. code-block:: python - :linenos: - - def includeme(config) - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -The user of the add-on package ``pyramid_subscriberhelpers`` would then be -able to install it and subsequently do: - -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - from pyramid.config import Configurator - config = Configurator() - config.include('pyramid_subscriberhelpers') - config.add_newrequest_subscriber(mysubscriber) +For more information, see the article :ref:`A Whirlwind Tour of Advanced +Configuration Tactics <cookbook:whirlwind-adv-conf>` in the Pyramid Community +Cookbook. diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 8d0e7058c..58f547fc9 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -7,8 +7,8 @@ Static Assets ============= -An :term:`asset` is any file contained within a Python :term:`package` which -is *not* a Python source code file. For example, each of the following is an +An :term:`asset` is any file contained within a Python :term:`package` which is +*not* a Python source code file. For example, each of the following is an asset: - a GIF image file contained within a Python package or contained within any @@ -20,20 +20,23 @@ asset: - a JavaScript source file contained within a Python package or contained within any subdirectory of a Python package. -- A directory within a package that does not have an ``__init__.py`` - in it (if it possessed an ``__init__.py`` it would *be* a package). +- A directory within a package that does not have an ``__init__.py`` in it (if + it possessed an ``__init__.py`` it would *be* a package). - a :term:`Chameleon` or :term:`Mako` template file contained within a Python package. The use of assets is quite common in most web development projects. For example, when you create a :app:`Pyramid` application using one of the -available scaffolds, as described in :ref:`creating_a_project`, the -directory representing the application contains a Python :term:`package`. -Within that Python package, there are directories full of files which are -static assets. For example, there's a ``static`` directory which contains -``.css``, ``.js``, and ``.gif`` files. These asset files are delivered when -a user visits an application URL. +available scaffolds, as described in :ref:`creating_a_project`, the directory +representing the application contains a Python :term:`package`. Within that +Python package, there are directories full of files which are static assets. +For example, there's a ``static`` directory which contains ``.css``, ``.js``, +and ``.gif`` files. These asset files are delivered when a user visits an +application URL. + +.. index:: + single: asset specifications .. _asset_specifications: @@ -42,12 +45,11 @@ Understanding Asset Specifications Let's imagine you've created a :app:`Pyramid` application that uses a :term:`Chameleon` ZPT template via the -:func:`pyramid.renderers.render_to_response` API. For example, the -application might address the asset using the :term:`asset specification` -``myapp:templates/some_template.pt`` using that API within a ``views.py`` -file inside a ``myapp`` package: +:func:`pyramid.renderers.render_to_response` API. For example, the application +might address the asset using the :term:`asset specification` +``myapp:templates/some_template.pt`` using that API within a ``views.py`` file +inside a ``myapp`` package: -.. ignore-next-block .. code-block:: python :linenos: @@ -64,27 +66,28 @@ two parts: - The *asset name* (``templates/some_template.pt``), relative to the package directory. -The two parts are separated by the colon character. +The two parts are separated by a colon ``:`` character. -:app:`Pyramid` uses the Python :term:`pkg_resources` API to resolve the -package name and asset name to an absolute (operating-system-specific) file -name. It eventually passes this resolved absolute filesystem path to the -Chameleon templating engine, which then uses it to load, parse, and execute -the template file. +:app:`Pyramid` uses the Python :term:`pkg_resources` API to resolve the package +name and asset name to an absolute (operating system-specific) file name. It +eventually passes this resolved absolute filesystem path to the Chameleon +templating engine, which then uses it to load, parse, and execute the template +file. There is a second form of asset specification: a *relative* asset specification. Instead of using an "absolute" asset specification which includes the package name, in certain circumstances you can omit the package name from the specification. For example, you might be able to use ``templates/mytemplate.pt`` instead of ``myapp:templates/some_template.pt``. -Such asset specifications are usually relative to a "current package." The +Such asset specifications are usually relative to a "current package". The "current package" is usually the package which contains the code that *uses* the asset specification. :app:`Pyramid` APIs which accept relative asset -specifications typically describe what the asset is relative to in their +specifications typically describe to what the asset is relative in their individual documentation. .. index:: single: add_static_view + pair: assets; serving .. _static_assets_section: @@ -93,15 +96,17 @@ Serving Static Assets :app:`Pyramid` makes it possible to serve up static asset files from a directory on a filesystem to an application user's browser. Use the -:meth:`pyramid.config.Configurator.add_static_view` to instruct -:app:`Pyramid` to serve static assets such as JavaScript and CSS files. This -mechanism makes a directory of static files available at a name relative to -the application root URL, e.g. ``/static`` or as an external URL. +:meth:`pyramid.config.Configurator.add_static_view` to instruct :app:`Pyramid` +to serve static assets, such as JavaScript and CSS files. This mechanism makes +a directory of static files available at a name relative to the application +root URL, e.g., ``/static``, or as an external URL. + +.. note:: -.. note:: :meth:`~pyramid.config.Configurator.add_static_view` cannot serve a - single file, nor can it serve a directory of static files directly - relative to the root URL of a :app:`Pyramid` application. For these - features, see :ref:`advanced_static`. + :meth:`~pyramid.config.Configurator.add_static_view` cannot serve a single + file, nor can it serve a directory of static files directly relative to the + root URL of a :app:`Pyramid` application. For these features, see + :ref:`advanced_static`. Here's an example of a use of :meth:`~pyramid.config.Configurator.add_static_view` that will serve files up @@ -114,13 +119,13 @@ from the ``/var/www/static`` directory of the computer which runs the # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='/var/www/static') -The ``name`` prepresents a URL *prefix*. In order for files that live in the +The ``name`` represents a URL *prefix*. In order for files that live in the ``path`` directory to be served, a URL that requests one of them must begin -with that prefix. In the example above, ``name`` is ``static``, and ``path`` -is ``/var/www/static``. In English, this means that you wish to serve the -files that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL -prefix. Therefore, the file ``/var/www/static/foo.css`` will be returned -when the user visits your application's URL ``/static/foo.css``. +with that prefix. In the example above, ``name`` is ``static`` and ``path`` is +``/var/www/static``. In English this means that you wish to serve the files +that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL prefix. +Therefore, the file ``/var/www/static/foo.css`` will be returned when the user +visits your application's URL ``/static/foo.css``. A static directory named at ``path`` may contain subdirectories recursively, and any subdirectories may hold files; these will be resolved by the static @@ -129,16 +134,16 @@ view for each particular type of file is dependent upon its file extension. By default, all files made available via :meth:`~pyramid.config.Configurator.add_static_view` are accessible by -completely anonymous users. Simple authorization can be required, however. -To protect a set of static files using a permission, in addition to passing -the required ``name`` and ``path`` arguments, also pass the ``permission`` -keyword argument to :meth:`~pyramid.config.Configurator.add_static_view`. -The value of the ``permission`` argument represents the :term:`permission` -that the user must have relative to the current :term:`context` when the -static view is invoked. A user will be required to possess this permission -to view any of the files represented by ``path`` of the static view. If your -static assets must be protected by a more complex authorization scheme, -see :ref:`advanced_static`. +completely anonymous users. Simple authorization can be required, however. To +protect a set of static files using a permission, in addition to passing the +required ``name`` and ``path`` arguments, also pass the ``permission`` keyword +argument to :meth:`~pyramid.config.Configurator.add_static_view`. The value of +the ``permission`` argument represents the :term:`permission` that the user +must have relative to the current :term:`context` when the static view is +invoked. A user will be required to possess this permission to view any of the +files represented by ``path`` of the static view. If your static assets must +be protected by a more complex authorization scheme, see +:ref:`advanced_static`. Here's another example that uses an :term:`asset specification` instead of an absolute path as the ``path`` argument. To convince @@ -158,45 +163,47 @@ may be a fully qualified :term:`asset specification` or an *absolute path*. Instead of representing a URL prefix, the ``name`` argument of a call to :meth:`~pyramid.config.Configurator.add_static_view` can alternately be a -*URL*. Each of examples we've seen so far have shown usage of the ``name`` -argument as a URL prefix. However, when ``name`` is a *URL*, static assets -can be served from an external webserver. In this mode, the ``name`` is used -as the URL prefix when generating a URL using :func:`pyramid.url.static_url`. +*URL*. Each of the examples we've seen so far have shown usage of the ``name`` +argument as a URL prefix. However, when ``name`` is a *URL*, static assets can +be served from an external webserver. In this mode, the ``name`` is used as +the URL prefix when generating a URL using +:meth:`pyramid.request.Request.static_url`. -For example, :meth:`~pyramid.config.Configurator.add_static_view` may -be fed a ``name`` argument which is ``http://example.com/images``: +For example, :meth:`~pyramid.config.Configurator.add_static_view` may be fed a +``name`` argument which is ``http://example.com/images``: .. code-block:: python :linenos: # config is an instance of pyramid.config.Configurator - config.add_static_view(name='http://example.com/images', + config.add_static_view(name='http://example.com/images', path='mypackage:images') -Because :meth:`~pyramid.config.Configurator.add_static_view` is provided with -a ``name`` argument that is the URL ``http://example.com/images``, subsequent -calls to :func:`~pyramid.url.static_url` with paths that start with the -``path`` argument passed to +Because :meth:`~pyramid.config.Configurator.add_static_view` is provided with a +``name`` argument that is the URL ``http://example.com/images``, subsequent +calls to :meth:`~pyramid.request.Request.static_url` with paths that start with +the ``path`` argument passed to :meth:`~pyramid.config.Configurator.add_static_view` will generate a URL -something like ``http://example.com/images/logo.png``. The external -webserver listening on ``example.com`` must be itself configured to respond -properly to such a request. The :func:`~pyramid.url.static_url` API is +something like ``http://example.com/images/logo.png``. The external webserver +listening on ``example.com`` must be itself configured to respond properly to +such a request. The :meth:`~pyramid.request.Request.static_url` API is discussed in more detail later in this chapter. .. index:: single: generating static asset urls single: static asset urls + pair: assets; generating urls .. _generating_static_asset_urls: Generating Static Asset URLs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When a :meth:`~pyramid.config.Configurator.add_static_view` method is used to +When an :meth:`~pyramid.config.Configurator.add_static_view` method is used to register a static asset directory, a special helper API named -:func:`pyramid.url.static_url` can be used to generate the appropriate URL -for an asset that lives in one of the directories named by the static -registration ``path`` attribute. +:meth:`pyramid.request.Request.static_url` can be used to generate the +appropriate URL for an asset that lives in one of the directories named by the +static registration ``path`` attribute. For example, let's assume you create a set of static declarations like so: @@ -206,82 +213,375 @@ For example, let's assume you create a set of static declarations like so: config.add_static_view(name='static1', path='mypackage:assets/1') config.add_static_view(name='static2', path='mypackage:assets/2') -These declarations create URL-accessible directories which have URLs that -begin with ``/static1`` and ``/static2``, respectively. The assets in the +These declarations create URL-accessible directories which have URLs that begin +with ``/static1`` and ``/static2``, respectively. The assets in the ``assets/1`` directory of the ``mypackage`` package are consulted when a user -visits a URL which begins with ``/static1``, and the assets in the -``assets/2`` directory of the ``mypackage`` package are consulted when a user -visits a URL which begins with ``/static2``. +visits a URL which begins with ``/static1``, and the assets in the ``assets/2`` +directory of the ``mypackage`` package are consulted when a user visits a URL +which begins with ``/static2``. You needn't generate the URLs to static assets "by hand" in such a -configuration. Instead, use the :func:`~pyramid.url.static_url` API to -generate them for you. For example: +configuration. Instead, use the :meth:`~pyramid.request.Request.static_url` +API to generate them for you. For example: .. code-block:: python :linenos: - from pyramid.url import static_url - from pyramid.chameleon_zpt import render_template_to_response + from pyramid.renderers import render_to_response def my_view(request): - css_url = static_url('mypackage:assets/1/foo.css', request) - js_url = static_url('mypackage:assets/2/foo.js', request) - return render_template_to_response('templates/my_template.pt', - css_url = css_url, - js_url = js_url) + css_url = request.static_url('mypackage:assets/1/foo.css') + js_url = request.static_url('mypackage:assets/2/foo.js') + return render_to_response('templates/my_template.pt', + dict(css_url=css_url, js_url=js_url), + request=request) If the request "application URL" of the running system is ``http://example.com``, the ``css_url`` generated above would be: -``http://example.com/static1/foo.css``. The ``js_url`` generated -above would be ``http://example.com/static2/foo.js``. +``http://example.com/static1/foo.css``. The ``js_url`` generated above would +be ``http://example.com/static2/foo.js``. -One benefit of using the :func:`~pyramid.url.static_url` function rather than -constructing static URLs "by hand" is that if you need to change the ``name`` -of a static URL declaration, the generated URLs will continue to resolve -properly after the rename. +One benefit of using the :meth:`~pyramid.request.Request.static_url` function +rather than constructing static URLs "by hand" is that if you need to change +the ``name`` of a static URL declaration, the generated URLs will continue to +resolve properly after the rename. -URLs may also be generated by :func:`~pyramid.url.static_url` to static assets -that live *outside* the :app:`Pyramid` application. This will happen when -the :meth:`~pyramid.config.Configurator.add_static_view` API associated with -the path fed to :func:`~pyramid.url.static_url` is a *URL* instead of a view -name. For example, the ``name`` argument may be ``http://example.com`` while -the the ``path`` given may be ``mypackage:images``: +URLs may also be generated by :meth:`~pyramid.request.Request.static_url` to +static assets that live *outside* the :app:`Pyramid` application. This will +happen when the :meth:`~pyramid.config.Configurator.add_static_view` API +associated with the path fed to :meth:`~pyramid.request.Request.static_url` is +a *URL* instead of a view name. For example, the ``name`` argument may be +``http://example.com`` while the ``path`` given may be ``mypackage:images``: .. code-block:: python :linenos: - config.add_static_view(name='http://example.com/images', + config.add_static_view(name='http://example.com/images', path='mypackage:images') -Under such a configuration, the URL generated by ``static_url`` for -assets which begin with ``mypackage:images`` will be prefixed with +Under such a configuration, the URL generated by ``static_url`` for assets +which begin with ``mypackage:images`` will be prefixed with ``http://example.com/images``: .. code-block:: python :linenos: - static_url('mypackage:images/logo.png', request) + request.static_url('mypackage:images/logo.png') # -> http://example.com/images/logo.png -Using :func:`~pyramid.url.static_url` in conjunction with a -:meth:`~pyramid.configuration.Configurator.add_static_view` makes it possible -to put static media on a separate webserver during production (if the -``name`` argument to :meth:`~pyramid.config.Configurator.add_static_view` is a -URL), while keeping static media package-internal and served by the -development webserver during development (if the ``name`` argument to -:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix). To -create such a circumstance, we suggest using the -:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting -in the application ``.ini`` file named ``media_location``. Then set the -value of ``media_location`` to either a prefix or a URL depending on whether -the application is being run in development or in production (use a different -``.ini`` file for production than you do for development). This is just a -suggestion for a pattern; any setting name other than ``media_location`` -could be used. +Using :meth:`~pyramid.request.Request.static_url` in conjunction with a +:meth:`~pyramid.config.Configurator.add_static_view` makes it possible to put +static media on a separate webserver during production (if the ``name`` +argument to :meth:`~pyramid.config.Configurator.add_static_view` is a URL), +while keeping static media package-internal and served by the development +webserver during development (if the ``name`` argument to +:meth:`~pyramid.config.Configurator.add_static_view` is a URL prefix). + +For example, we may define a :ref:`custom setting <adding_a_custom_setting>` +named ``media_location`` which we can set to an external URL in production when +our assets are hosted on a CDN. + +.. code-block:: python + :linenos: + + media_location = settings.get('media_location', 'static') + + config = Configurator(settings=settings) + config.add_static_view(path='myapp:static', name=media_location) + +Now we can optionally define the setting in our ini file: + +.. code-block:: ini + :linenos: + + # production.ini + [app:main] + use = egg:myapp#main + + media_location = http://static.example.com/ + +It is also possible to serve assets that live outside of the source by +referring to an absolute path on the filesystem. There are two ways to +accomplish this. + +First, :meth:`~pyramid.config.Configurator.add_static_view` supports taking an +absolute path directly instead of an asset spec. This works as expected, +looking in the file or folder of files and serving them up at some URL within +your application or externally. Unfortunately, this technique has a drawback in +that it is not possible to use the :meth:`~pyramid.request.Request.static_url` +method to generate URLs, since it works based on an asset specification. + +.. versionadded:: 1.6 + +The second approach, available in Pyramid 1.6+, uses the asset overriding APIs +described in the :ref:`overriding_assets_section` section. It is then possible +to configure a "dummy" package which then serves its file or folder from an +absolute path. + +.. code-block:: python + + config.add_static_view(path='myapp:static_images', name='static') + config.override_asset(to_override='myapp:static_images/', + override_with='/abs/path/to/images/') + +From this configuration it is now possible to use +:meth:`~pyramid.request.Request.static_url` to generate URLs to the data in the +folder by doing something like +``request.static_url('myapp:static_images/foo.png')``. While it is not +necessary that the ``static_images`` file or folder actually exist in the +``myapp`` package, it is important that the ``myapp`` portion points to a valid +package. If the folder does exist, then the overriden folder is given priority, +if the file's name exists in both locations. + +.. index:: + single: Cache Busting + +.. _cache_busting: + +Cache Busting +------------- + +.. versionadded:: 1.6 + +In order to maximize performance of a web application, you generally want to +limit the number of times a particular client requests the same static asset. +Ideally a client would cache a particular static asset "forever", requiring it +to be sent to the client a single time. The HTTP protocol allows you to send +headers with an HTTP response that can instruct a client to cache a particular +asset for an amount of time. As long as the client has a copy of the asset in +its cache and that cache hasn't expired, the client will use the cached copy +rather than request a new copy from the server. The drawback to sending cache +headers to the client for a static asset is that at some point the static asset +may change, and then you'll want the client to load a new copy of the asset. +Under normal circumstances you'd just need to wait for the client's cached copy +to expire before they get the new version of the static resource. + +A commonly used workaround to this problem is a technique known as +:term:`cache busting`. Cache busting schemes generally involve generating a +URL for a static asset that changes when the static asset changes. This way +headers can be sent along with the static asset instructing the client to cache +the asset for a very long time. When a static asset is changed, the URL used +to refer to it in a web page also changes, so the client sees it as a new +resource and requests the asset, regardless of any caching policy set for the +resource's old URL. + +:app:`Pyramid` can be configured to produce cache busting URLs for static +assets using :meth:`~pyramid.config.Configurator.add_cache_buster`: + +.. code-block:: python + :linenos: + + import time + from pyramid.static import QueryStringConstantCacheBuster + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='static', path='mypackage:folder/static/') + config.add_cache_buster( + 'mypackage:folder/static/', + QueryStringConstantCacheBuster(str(int(time.time())))) + +Adding the cachebuster instructs :app:`Pyramid` to add the current time for +a static asset to the query string in the asset's URL: + +.. code-block:: python + :linenos: + + js_url = request.static_url('mypackage:folder/static/js/myapp.js') + # Returns: 'http://www.example.com/static/js/myapp.js?x=1445318121' + +When the web server restarts, the time constant will change and therefore so +will its URL. + +.. note:: + + Cache busting is an inherently complex topic as it integrates the asset + pipeline and the web application. It is expected and desired that + application authors will write their own cache buster implementations + conforming to the properties of their own asset pipelines. See + :ref:`custom_cache_busters` for information on writing your own. + +Disabling the Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It can be useful in some situations (e.g., development) to globally disable all +configured cache busters without changing calls to +:meth:`~pyramid.config.Configurator.add_cache_buster`. To do this set the +``PYRAMID_PREVENT_CACHEBUST`` environment variable or the +``pyramid.prevent_cachebust`` configuration value to a true value. + +.. _custom_cache_busters: + +Customizing the Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Calls to :meth:`~pyramid.config.Configurator.add_cache_buster` may use +any object that implements the interface +:class:`~pyramid.interfaces.ICacheBuster`. + +:app:`Pyramid` ships with a very simplistic +:class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an +arbitrary token you provide to the query string of the asset's URL. This +is almost never what you want in production as it does not allow fine-grained +busting of individual assets. + +In order to implement your own cache buster, you can write your own class from +scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` +interface. Alternatively you may choose to subclass one of the existing +implementations. One of the most likely scenarios is you'd want to change the +way the asset token is generated. To do this just subclass +:class:`~pyramid.static.QueryStringCacheBuster` and define a +``tokenize(pathspec)`` method. Here is an example which uses Git to get +the hash of the current commit: + +.. code-block:: python + :linenos: + + import os + import subprocess + from pyramid.static import QueryStringCacheBuster + + class GitCacheBuster(QueryStringCacheBuster): + """ + Assuming your code is installed as a Git checkout, as opposed to an egg + from an egg repository like PYPI, you can use this cachebuster to get + the current commit's SHA1 to use as the cache bust token. + """ + def __init__(self, param='x', repo_path=None): + super(GitCacheBuster, self).__init__(param=param) + if repo_path is None: + repo_path = os.path.dirname(os.path.abspath(__file__)) + self.sha1 = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], + cwd=repo_path).strip() + + def tokenize(self, pathspec): + return self.sha1 + +A simple cache buster that modifies the path segment can be constructed as +well: + +.. code-block:: python + :linenos: + + import posixpath + + class PathConstantCacheBuster(object): + def __init__(self, token): + self.token = token + + def __call__(self, request, subpath, kw): + base_subpath, ext = posixpath.splitext(subpath) + new_subpath = base_subpath + self.token + ext + return new_subpath, kw + +The caveat with this approach is that modifying the path segment +changes the file name, and thus must match what is actually on the +filesystem in order for :meth:`~pyramid.config.Configurator.add_static_view` +to find the file. It's better to use the +:class:`~pyramid.static.ManifestCacheBuster` for these situations, as +described in the next section. + +.. _path_segment_cache_busters: + +Path Segments and Choosing a Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many caching HTTP proxies will fail to cache a resource if the URL contains +a query string. Therefore, in general, you should prefer a cache busting +strategy which modifies the path segment rather than methods which add a +token to the query string. + +You will need to consider whether the :app:`Pyramid` application will be +serving your static assets, whether you are using an external asset pipeline +to handle rewriting urls internal to the css/javascript, and how fine-grained +do you want the cache busting tokens to be. + +In many cases you will want to host the static assets on another web server +or externally on a CDN. In these cases your :app:`Pyramid` application may not +even have access to a copy of the static assets. In order to cache bust these +assets you will need some information about them. + +If you are using an external asset pipeline to generate your static files you +should consider using the :class:`~pyramid.static.ManifestCacheBuster`. +This cache buster can load a standard JSON formatted file generated by your +pipeline and use it to cache bust the assets. This has many performance +advantages as :app:`Pyramid` does not need to look at the files to generate +any cache busting tokens, but still supports fine-grained per-file tokens. + +Assuming an example ``manifest.json`` like: + +.. code-block:: json + + { + "css/main.css": "css/main-678b7c80.css", + "images/background.png": "images/background-a8169106.png" + } + +The following code would set up a cachebuster: + +.. code-block:: python + :linenos: + + from pyramid.static import ManifestCacheBuster + + config.add_static_view( + name='http://mycdn.example.com/', + path='mypackage:static') + + config.add_cache_buster( + 'mypackage:static/', + ManifestCacheBuster('myapp:static/manifest.json')) + +It's important to note that the cache buster only handles generating +cache-busted URLs for static assets. It does **NOT** provide any solutions for +serving those assets. For example, if you generated a URL for +``css/main-678b7c80.css`` then that URL needs to be valid either by +configuring ``add_static_view`` properly to point to the location of the files +or some other mechanism such as the files existing on your CDN or rewriting +the incoming URL to remove the cache bust tokens. .. index:: single: static assets view +CSS and JavaScript source and cache busting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Often one needs to refer to images and other static assets inside CSS and +JavaScript files. If cache busting is active, the final static asset URL is not +available until the static assets have been assembled. These URLs cannot be +handwritten. Below is an example of how to integrate the cache buster into +the entire stack. Remember, it is just an example and should be modified to +fit your specific tools. + +* First, process the files by using a precompiler which rewrites URLs to their + final cache-busted form. Then, you can use the + :class:`~pyramid.static.ManifestCacheBuster` to synchronize your asset + pipeline with :app:`Pyramid`, allowing the pipeline to have full control + over the final URLs of your assets. + +Now that you are able to generate static URLs within :app:`Pyramid`, +you'll need to handle URLs that are out of our control. To do this you may +use some of the following options to get started: + +* Configure your asset pipeline to rewrite URL references inline in + CSS and JavaScript. This is the best approach because then the files + may be hosted by :app:`Pyramid` or an external CDN without having to + change anything. They really are static. + +* Templatize JS and CSS, and call ``request.static_url()`` inside their + template code. While this approach may work in certain scenarios, it is not + recommended because your static assets will not really be static and are now + dependent on :app:`Pyramid` to be served correctly. See + :ref:`advanced_static` for more information on this approach. + +If your CSS and JavaScript assets use URLs to reference other assets it is +recommended that you implement an external asset pipeline that can rewrite the +generated static files with new URLs containing cache busting tokens. The +machinery inside :app:`Pyramid` will not help with this step as it has very +little knowledge of the asset types your application may use. The integration +into :app:`Pyramid` is simply for linking those assets into your HTML and +other dynamic content. + .. _advanced_static: Advanced: Serving Static Assets Using a View Callable @@ -289,46 +589,51 @@ Advanced: Serving Static Assets Using a View Callable For more flexibility, static assets can be served by a :term:`view callable` which you register manually. For example, if you're using :term:`URL -dispatch`, you may want static assets to only be available as a fallback if -no previous route matches. Alternately, you might like to serve a particular +dispatch`, you may want static assets to only be available as a fallback if no +previous route matches. Alternatively, you might like to serve a particular static asset manually, because its download requires authentication. -Note that you cannot use the :func:`~pyramid.url.static_url` API to generate -URLs against assets made accessible by registering a custom static view. +Note that you cannot use the :meth:`~pyramid.request.Request.static_url` API to +generate URLs against assets made accessible by registering a custom static +view. Root-Relative Custom Static View (URL Dispatch Only) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :class:`pyramid.view.static` helper class generates a Pyramid view +The :class:`pyramid.static.static_view` helper class generates a Pyramid view callable. This view callable can serve static assets from a directory. An instance of this class is actually used by the :meth:`~pyramid.config.Configurator.add_static_view` configuration method, so its behavior is almost exactly the same once it's configured. -.. warning:: The following example *will not work* for applications that use - :term:`traversal`, it will only work if you use :term:`URL dispatch` +.. warning:: + + The following example *will not work* for applications that use + :term:`traversal`; it will only work if you use :term:`URL dispatch` exclusively. The root-relative route we'll be registering will always be matched before traversal takes place, subverting any views registered via ``add_view`` (at least those without a ``route_name``). A - :class:`~pyramid.view.static` static view cannot be made root-relative when - you use traversal. + :class:`~pyramid.static.static_view` static view cannot be made + root-relative when you use traversal unless it's registered as a :term:`Not + Found View`. To serve files within a directory located on your filesystem at ``/path/to/static/dir`` as the result of a "catchall" route hanging from the root that exists at the end of your routing table, create an instance of the -:class:`~pyramid.view.static` class inside a ``static.py`` file in your +:class:`~pyramid.static.static_view` class inside a ``static.py`` file in your application root as below. -.. ignore-next-block .. code-block:: python :linenos: - from pyramid.view import static - static_view = static('/path/to/static/dir') + from pyramid.static import static_view + static_view = static_view('/path/to/static/dir', use_subpath=True) -.. note:: For better cross-system flexibility, use an :term:`asset - specification` as the argument to :class:`~pyramid.view.static` instead of - a physical absolute filesystem path, e.g. ``mypackage:static`` instead of +.. note:: + + For better cross-system flexibility, use an :term:`asset specification` as + the argument to :class:`~pyramid.static.static_view` instead of a physical + absolute filesystem path, e.g., ``mypackage:static``, instead of ``/path/to/mypackage/static``. Subsequently, you may wire the files that are served by this view up to be @@ -345,35 +650,36 @@ application's startup code. config.add_view('myapp.static.static_view', route_name='catchall_static') The special name ``*subpath`` above is used by the -:class:`~pyramid.view.static` view callable to signify the path of the file -relative to the directory you're serving. +:class:`~pyramid.static.static_view` view callable to signify the path of the +file relative to the directory you're serving. -Registering A View Callable to Serve a "Static" Asset +Registering a View Callable to Serve a "Static" Asset ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can register a simple view callable to serve a single static asset. To -do so, do things "by hand". First define the view callable. +You can register a simple view callable to serve a single static asset. To do +so, do things "by hand". First define the view callable. .. code-block:: python :linenos: import os - from webob import Response + from pyramid.response import FileResponse def favicon_view(request): here = os.path.dirname(__file__) - icon = open(os.path.join(here, 'static', 'favicon.ico')) - return Response(content_type='image/x-icon', app_iter=icon) + icon = os.path.join(here, 'static', 'favicon.ico') + return FileResponse(icon, request=request) -The above bit of code within ``favicon_view`` computes "here", which is a -path relative to the Python file in which the function is defined. It then -uses the Python ``open`` function to obtain a file handle to a file within -"here" named ``static``, and returns a response using the open the file -handle as the response's ``app_iter``. It makes sure to set the right -content_type too. +The above bit of code within ``favicon_view`` computes "here", which is a path +relative to the Python file in which the function is defined. It then creates +a :class:`pyramid.response.FileResponse` using the file path as the response's +``path`` argument and the request as the response's ``request`` argument. +:class:`pyramid.response.FileResponse` will serve the file as quickly as +possible when it's used this way. It makes sure to set the right content +length and content_type, too, based on the file extension of the file you pass. -You might register such a view via configuration as a view callable that -should be called as the result of a traversal: +You might register such a view via configuration as a view callable that should +be called as the result of a traversal: .. code-block:: python :linenos: @@ -406,13 +712,12 @@ It can often be useful to override specific assets from "outside" a given :app:`Pyramid` application more or less unchanged. However, some specific template file owned by the application might have inappropriate HTML, or some static asset (such as a logo file or some CSS file) might not be appropriate. -You *could* just fork the application entirely, but it's often more -convenient to just override the assets that are inappropriate and reuse the -application "as is". This is particularly true when you reuse some "core" -application over and over again for some set of customers (such as a CMS -application, or some bug tracking application), and you want to make -arbitrary visual modifications to a particular application deployment without -forking the underlying code. +You *could* just fork the application entirely, but it's often more convenient +to just override the assets that are inappropriate and reuse the application +"as is". This is particularly true when you reuse some "core" application over +and over again for some set of customers (such as a CMS application, or some +bug tracking application), and you want to make arbitrary visual modifications +to a particular application deployment without forking the underlying code. To this end, :app:`Pyramid` contains a feature that makes it possible to "override" one asset with one or more other assets. In support of this @@ -420,21 +725,18 @@ feature, a :term:`Configurator` API exists named :meth:`pyramid.config.Configurator.override_asset`. This API allows you to *override* the following kinds of assets defined in any Python package: -- Individual :term:`Chameleon` templates. +- Individual template files. -- A directory containing multiple Chameleon templates. +- A directory containing multiple template files. - Individual static files served up by an instance of the - ``pyramid.view.static`` helper class. + ``pyramid.static.static_view`` helper class. - A directory of static files served up by an instance of the - ``pyramid.view.static`` helper class. - -- Any other asset (or set of assets) addressed by code that uses the - setuptools :term:`pkg_resources` API. + ``pyramid.static.static_view`` helper class. -.. note:: The :term:`ZCML` directive named ``asset`` serves the same purpose - as the :meth:`~pyramid.config.Configurator.override_asset` method. +- Any other asset (or set of assets) addressed by code that uses the setuptools + :term:`pkg_resources` API. .. index:: single: override_asset @@ -444,25 +746,23 @@ feature, a :term:`Configurator` API exists named The ``override_asset`` API ~~~~~~~~~~~~~~~~~~~~~~~~~~ -An individual call to :meth:`~pyramid.config.Configurator.override_asset` -can override a single asset. For example: +An individual call to :meth:`~pyramid.config.Configurator.override_asset` can +override a single asset. For example: -.. ignore-next-block .. code-block:: python :linenos: config.override_asset( - to_override='some.package:templates/mytemplate.pt', - override_with='another.package:othertemplates/anothertemplate.pt') + to_override='some.package:templates/mytemplate.pt', + override_with='another.package:othertemplates/anothertemplate.pt') The string value passed to both ``to_override`` and ``override_with`` sent to -the ``override_asset`` API is called an :term:`asset specification`. The -colon separator in a specification separates the *package name* from the -*asset name*. The colon and the following asset name are optional. If they -are not specified, the override attempts to resolve every lookup into a -package from the directory of another package. For example: +the ``override_asset`` API is called an :term:`asset specification`. The colon +separator in a specification separates the *package name* from the *asset +name*. The colon and the following asset name are optional. If they are not +specified, the override attempts to resolve every lookup into a package from +the directory of another package. For example: -.. ignore-next-block .. code-block:: python :linenos: @@ -471,51 +771,104 @@ package from the directory of another package. For example: Individual subdirectories within a package can also be overridden: -.. ignore-next-block .. code-block:: python :linenos: config.override_asset(to_override='some.package:templates/', override_with='another.package:othertemplates/') - -If you wish to override a directory with another directory, you *must* -make sure to attach the slash to the end of both the ``to_override`` -specification and the ``override_with`` specification. If you fail to -attach a slash to the end of a specification that points to a directory, -you will get unexpected results. +If you wish to override a directory with another directory, you *must* make +sure to attach the slash to the end of both the ``to_override`` specification +and the ``override_with`` specification. If you fail to attach a slash to the +end of a specification that points to a directory, you will get unexpected +results. You cannot override a directory specification with a file specification, and -vice versa: a startup error will occur if you try. You cannot override an -asset with itself: a startup error will occur if you try. +vice versa; a startup error will occur if you try. You cannot override an +asset with itself; a startup error will occur if you try. Only individual *package* assets may be overridden. Overrides will not -traverse through subpackages within an overridden package. This means that -if you want to override assets for both ``some.package:templates``, and +traverse through subpackages within an overridden package. This means that if +you want to override assets for both ``some.package:templates``, and ``some.package.views:templates``, you will need to register two overrides. -The package name in a specification may start with a dot, meaning that -the package is relative to the package in which the configuration -construction file resides (or the ``package`` argument to the -:class:`~pyramid.config.Configurator` class construction). -For example: +The package name in a specification may start with a dot, meaning that the +package is relative to the package in which the configuration construction file +resides (or the ``package`` argument to the +:class:`~pyramid.config.Configurator` class construction). For example: -.. ignore-next-block .. code-block:: python :linenos: config.override_asset(to_override='.subpackage:templates/', override_with='another.package:templates/') -Multiple calls to ``override_asset`` which name a shared ``to_override`` but -a different ``override_with`` specification can be "stacked" to form a search -path. The first asset that exists in the search path will be used; if no -asset exists in the override path, the original asset is used. +Multiple calls to ``override_asset`` which name a shared ``to_override`` but a +different ``override_with`` specification can be "stacked" to form a search +path. The first asset that exists in the search path will be used; if no asset +exists in the override path, the original asset is used. Asset overrides can actually override assets other than templates and static files. Any software which uses the :func:`pkg_resources.get_resource_filename`, -:func:`pkg_resources.get_resource_stream` or +:func:`pkg_resources.get_resource_stream`, or :func:`pkg_resources.get_resource_string` APIs will obtain an overridden file when an override is used. +.. versionadded:: 1.6 + As of Pyramid 1.6, it is also possible to override an asset by supplying an + absolute path to a file or directory. This may be useful if the assets are + not distributed as part of a Python package. + +Cache Busting and Asset Overrides +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Overriding static assets that are being hosted using +:meth:`pyramid.config.Configurator.add_static_view` can affect your cache +busting strategy when using any cache busters that are asset-aware such as +:class:`pyramid.static.ManifestCacheBuster`. What sets asset-aware cache +busters apart is that they have logic tied to specific assets. For example, +a manifest is only generated for a specific set of pre-defined assets. Now, +imagine you have overridden an asset defined in this manifest with a new, +unknown version. By default, the cache buster will be invoked for an asset +it has never seen before and will likely end up returning a cache busting +token for the original asset rather than the asset that will actually end up +being served! In order to get around this issue, it's possible to attach a +different :class:`pyramid.interfaces.ICacheBuster` implementation to the +new assets. This would cause the original assets to be served by their +manifest, and the new assets served by their own cache buster. To do this, +:meth:`pyramid.config.Configurator.add_cache_buster` supports an ``explicit`` +option. For example: + +.. code-block:: python + :linenos: + + from pyramid.static import ManifestCacheBuster + + # define a static view for myapp:static assets + config.add_static_view('static', 'myapp:static') + + # setup a cache buster for your app based on the myapp:static assets + my_cb = ManifestCacheBuster('myapp:static/manifest.json') + config.add_cache_buster('myapp:static', my_cb) + + # override an asset + config.override_asset( + to_override='myapp:static/background.png', + override_with='theme:static/background.png') + + # override the cache buster for theme:static assets + theme_cb = ManifestCacheBuster('theme:static/manifest.json') + config.add_cache_buster('theme:static', theme_cb, explicit=True) + +In the above example there is a default cache buster, ``my_cb``, for all +assets served from the ``myapp:static`` folder. This would also affect +``theme:static/background.png`` when generating URLs via +``request.static_url('myapp:static/background.png')``. + +The ``theme_cb`` is defined explicitly for any assets loaded from the +``theme:static`` folder. Explicit cache busters have priority and thus +``theme_cb`` would be invoked for +``request.static_url('myapp:static/background.png')``, but ``my_cb`` would +be used for any other assets like +``request.static_url('myapp:static/favicon.ico')``. diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst new file mode 100644 index 000000000..6cd90d42f --- /dev/null +++ b/docs/narr/commandline.rst @@ -0,0 +1,1057 @@ +.. _command_line_chapter: + +Command-Line Pyramid +==================== + +Your :app:`Pyramid` application can be controlled and inspected using a variety +of command-line utilities. These utilities are documented in this chapter. + + +.. index:: + pair: matching views; printing + single: pviews + +.. _displaying_matching_views: + +Displaying Matching Views for a Given URL +----------------------------------------- + +.. seealso:: See also the output of :ref:`pviews --help <pviews_script>`. + +For a big application with several views, it can be hard to keep the view +configuration details in your head, even if you defined all the views yourself. +You can use the ``pviews`` command in a terminal window to print a summary of +matching routes and views for a given URL in your application. The ``pviews`` +command accepts two arguments. The first argument to ``pviews`` is the path to +your application's ``.ini`` file and section name inside the ``.ini`` file +which points to your application. This should be of the format +``config_file#section_name``. The second argument is the URL to test for +matching views. The ``section_name`` may be omitted; if it is, it's considered +to be ``main``. + +Here is an example for a simple view configuration using :term:`traversal`: + +.. code-block:: text + :linenos: + + $ $VENV/bin/pviews development.ini#tutorial /FrontPage + + URL = /FrontPage + + context: <tutorial.models.Page object at 0xa12536c> + view name: + + View: + ----- + tutorial.views.view_page + required permission = view + +The output always has the requested URL at the top and below that all the views +that matched with their view configuration details. In this example only one +view matches, so there is just a single *View* section. For each matching view, +the full code path to the associated view callable is shown, along with any +permissions and predicates that are part of that view configuration. + +A more complex configuration might generate something like this: + +.. code-block:: text + :linenos: + + $ $VENV/bin/pviews development.ini#shootout /about + + URL = /about + + context: <shootout.models.RootFactory object at 0xa56668c> + view name: about + + Route: + ------ + route name: about + route pattern: /about + route path: /about + subpath: + route predicates (request method = GET) + + View: + ----- + shootout.views.about_view + required permission = view + view predicates (request_param testing, header X/header) + + Route: + ------ + route name: about_post + route pattern: /about + route path: /about + subpath: + route predicates (request method = POST) + + View: + ----- + shootout.views.about_view_post + required permission = view + view predicates (request_param test) + + View: + ----- + shootout.views.about_view_post2 + required permission = view + view predicates (request_param test2) + +In this case, we are dealing with a :term:`URL dispatch` application. This +specific URL has two matching routes. The matching route information is +displayed first, followed by any views that are associated with that route. As +you can see from the second matching route output, a route can be associated +with more than one view. + +For a URL that doesn't match any views, ``pviews`` will simply print out a *Not +found* message. + + +.. index:: + single: interactive shell + single: pshell + +.. _interactive_shell: + +The Interactive Shell +--------------------- + +.. seealso:: See also the output of :ref:`pshell --help <pshell_script>`. + +Once you've installed your program for development using ``pip install -e .``, +you can use an interactive Python shell to execute expressions in a Python +environment exactly like the one that will be used when your application runs +"for real". To do so, use the ``pshell`` command line utility. + +The argument to ``pshell`` follows the format ``config_file#section_name`` +where ``config_file`` is the path to your application's ``.ini`` file and +``section_name`` is the ``app`` section name inside the ``.ini`` file which +points to your application. For example, your application ``.ini`` file might +have an ``[app:main]`` section that looks like so: + +.. code-block:: ini + :linenos: + + [app:main] + use = egg:MyProject + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.debug_templates = true + pyramid.default_locale_name = en + +If so, you can use the following command to invoke a debug shell using the name +``main`` as a section name: + +.. code-block:: text + + $ $VENV/bin/pshell starter/development.ini#main + Python 2.6.5 (r265:79063, Apr 29 2010, 00:31:32) + [GCC 4.4.3] on linux2 + Type "help" for more information. + + Environment: + app The WSGI application. + registry Active Pyramid registry. + request Active request object. + root Root of the default resource tree. + root_factory Default root factory used to create `root`. + + >>> root + <myproject.resources.MyResource object at 0x445270> + >>> registry + <Registry myproject> + >>> registry.settings['pyramid.debug_notfound'] + False + >>> from myproject.views import my_view + >>> from pyramid.request import Request + >>> r = Request.blank('/') + >>> my_view(r) + {'project': 'myproject'} + +The WSGI application that is loaded will be available in the shell as the +``app`` global. Also, if the application that is loaded is the :app:`Pyramid` +app with no surrounding :term:`middleware`, the ``root`` object returned by the +default :term:`root factory`, ``registry``, and ``request`` will be available. + +You can also simply rely on the ``main`` default section name by omitting any +hash after the filename: + +.. code-block:: text + + $ $VENV/bin/pshell starter/development.ini + +Press ``Ctrl-D`` to exit the interactive shell (or ``Ctrl-Z`` on Windows). + + +.. index:: + pair: pshell; extending + +.. _extending_pshell: + +Extending the Shell +~~~~~~~~~~~~~~~~~~~ + +It is convenient when using the interactive shell often to have some variables +significant to your application already loaded as globals when you start the +``pshell``. To facilitate this, ``pshell`` will look for a special ``[pshell]`` +section in your INI file and expose the subsequent key/value pairs to the +shell. Each key is a variable name that will be global within the pshell +session; each value is a :term:`dotted Python name`. If specified, the special +key ``setup`` should be a :term:`dotted Python name` pointing to a callable +that accepts the dictionary of globals that will be loaded into the shell. This +allows for some custom initializing code to be executed each time the +``pshell`` is run. The ``setup`` callable can also be specified from the +commandline using the ``--setup`` option which will override the key in the INI +file. + +For example, you want to expose your model to the shell along with the database +session so that you can mutate the model on an actual database. Here, we'll +assume your model is stored in the ``myapp.models`` package. + +.. code-block:: ini + :linenos: + + [pshell] + setup = myapp.lib.pshell.setup + m = myapp.models + session = myapp.models.DBSession + t = transaction + +By defining the ``setup`` callable, we will create the module +``myapp.lib.pshell`` containing a callable named ``setup`` that will receive +the global environment before it is exposed to the shell. Here we mutate the +environment's request as well as add a new value containing a WebTest version +of the application to which we can easily submit requests. + +.. code-block:: python + :linenos: + + # myapp/lib/pshell.py + from webtest import TestApp + + def setup(env): + env['request'].host = 'www.example.com' + env['request'].scheme = 'https' + env['testapp'] = TestApp(env['app']) + +When this INI file is loaded, the extra variables ``m``, ``session`` and ``t`` +will be available for use immediately. Since a ``setup`` callable was also +specified, it is executed and a new variable ``testapp`` is exposed, and the +request is configured to generate urls from the host +``http://www.example.com``. For example: + +.. code-block:: text + + $ $VENV/bin/pshell starter/development.ini + Python 2.6.5 (r265:79063, Apr 29 2010, 00:31:32) + [GCC 4.4.3] on linux2 + Type "help" for more information. + + Environment: + app The WSGI application. + registry Active Pyramid registry. + request Active request object. + root Root of the default resource tree. + root_factory Default root factory used to create `root`. + testapp <webtest.TestApp object at ...> + + Custom Variables: + m myapp.models + session myapp.models.DBSession + t transaction + + >>> testapp.get('/') + <200 OK text/html body='<!DOCTYPE...l>\n'/3337> + >>> request.route_url('home') + 'https://www.example.com/' + + +.. _ipython_or_bpython: + +Alternative Shells +~~~~~~~~~~~~~~~~~~ + +The ``pshell`` command can be easily extended with alternate REPLs if the +default python REPL is not satisfactory. Assuming you have a binding +installed such as ``pyramid_ipython`` it will normally be auto-selected and +used. You may also specifically invoke your choice with the ``-p choice`` or +``--python-shell choice`` option. + +.. code-block:: text + + $ $VENV/bin/pshell -p ipython development.ini#MyProject + +You may use the ``--list-shells`` option to see the available shells. + +.. code-block:: text + + $ $VENV/bin/pshell --list-shells + Available shells: + bpython + ipython + python + +If you want to use a shell that isn't supported out of the box, you can +introduce a new shell by registering an entry point in your ``setup.py``: + +.. code-block:: python + + setup( + entry_points={ + 'pyramid.pshell_runner': [ + 'myshell=my_app:ptpython_shell_factory', + ], + }, + ) + +And then your shell factory should return a function that accepts two +arguments, ``env`` and ``help``, which would look like this: + +.. code-block:: python + + from ptpython.repl import embed + + def ptpython_shell_runner(env, help): + print(help) + return embed(locals=env) + +.. versionchanged:: 1.6 + User-defined shells may be registered using entry points. Prior to this + the only supported shells were ``ipython``, ``bpython`` and ``python``. + + ``ipython`` and ``bpython`` have been moved into their respective + packages ``pyramid_ipython`` and ``pyramid_bpython``. + + +Setting a Default Shell +~~~~~~~~~~~~~~~~~~~~~~~ + +You may use the ``default_shell`` option in your ``[pshell]`` ini section to +specify a list of preferred shells. + +.. code-block:: ini + :linenos: + + [pshell] + default_shell = ptpython ipython bpython + +.. versionadded:: 1.6 + + +.. index:: + pair: routes; printing + single: proutes + +.. _displaying_application_routes: + +Displaying All Application Routes +--------------------------------- + +.. seealso:: See also the output of :ref:`proutes --help <proutes_script>`. + +You can use the ``proutes`` command in a terminal window to print a summary of +routes related to your application. Much like the ``pshell`` command (see +:ref:`interactive_shell`), the ``proutes`` command accepts one argument with +the format ``config_file#section_name``. The ``config_file`` is the path to +your application's ``.ini`` file, and ``section_name`` is the ``app`` section +name inside the ``.ini`` file which points to your application. By default, +the ``section_name`` is ``main`` and can be omitted. + +For example: + +.. code-block:: text + :linenos: + + $ $VENV/bin/proutes development.ini + Name Pattern View Method + ---- ------- ---- ------ + debugtoolbar /_debug_toolbar/*subpath <wsgiapp> * + __static/ /static/*subpath dummy_starter:static/ * + __static2/ /static2/*subpath /var/www/static/ * + __pdt_images/ /pdt_images/*subpath pyramid_debugtoolbar:static/img/ * + a / <unknown> * + no_view_attached / <unknown> * + route_and_view_attached / app1.standard_views.route_and_view_attached * + method_conflicts /conflicts app1.standard_conflicts <route mismatch> + multiview /multiview app1.standard_views.multiview GET,PATCH + not_post /not_post app1.standard_views.multview !POST,* + +``proutes`` generates a table with four columns: *Name*, *Pattern*, *View*, and +*Method*. The items listed in the Name column are route names, the items +listed in the Pattern column are route patterns, the items listed in the View +column are representations of the view callable that will be invoked when a +request matches the associated route pattern, and the items listed in the +Method column are the request methods that are associated with the route name. +The View column may show ``<unknown>`` if no associated view callable could be +found. The Method column, for the route name, may show either ``<route +mismatch>`` if the view callable does not accept any of the route's request +methods, or ``*`` if the view callable will accept any of the route's request +methods. If no routes are configured within your application, nothing will be +printed to the console when ``proutes`` is executed. + +It is convenient when using the ``proutes`` command often to configure which +columns and the order you would like to view them. To facilitate this, +``proutes`` will look for a special ``[proutes]`` section in your ``.ini`` file +and use those as defaults. + +For example you may remove the request method and place the view first: + +.. code-block:: text + :linenos: + + [proutes] + format = view + name + pattern + +You can also separate the formats with commas or spaces: + +.. code-block:: text + :linenos: + + [proutes] + format = view name pattern + + [proutes] + format = view, name, pattern + +If you want to temporarily configure the columns and order, there is the +argument ``--format``, which is a comma separated list of columns you want to +include. The current available formats are ``name``, ``pattern``, ``view``, and +``method``. + + +.. index:: + pair: tweens; printing + single: ptweens + +.. _displaying_tweens: + +Displaying "Tweens" +------------------- + +.. seealso:: See also the output of :ref:`ptweens --help <ptweens_script>`. + +A :term:`tween` is a bit of code that sits between the main Pyramid application +request handler and the WSGI application which calls it. A user can get a +representation of both the implicit tween ordering (the ordering specified by +calls to :meth:`pyramid.config.Configurator.add_tween`) and the explicit tween +ordering (specified by the ``pyramid.tweens`` configuration setting) using the +``ptweens`` command. Tween factories will show up represented by their +standard Python dotted name in the ``ptweens`` output. + +For example, here's the ``ptweens`` command run against a system configured +without any explicit tweens: + +.. code-block:: text + :linenos: + + $ $VENV/bin/ptweens development.ini + "pyramid.tweens" config value NOT set (implicitly ordered tweens used) + + Implicit Tween Chain + + Position Name Alias + -------- ---- ----- + - - INGRESS + 0 pyramid_debugtoolbar.toolbar.toolbar_tween_factory pdbt + 1 pyramid.tweens.excview_tween_factory excview + - - MAIN + +Here's the ``ptweens`` command run against a system configured *with* explicit +tweens defined in its ``development.ini`` file: + +.. code-block:: text + :linenos: + + $ ptweens development.ini + "pyramid.tweens" config value set (explicitly ordered tweens used) + + Explicit Tween Chain (used) + + Position Name + -------- ---- + - INGRESS + 0 starter.tween_factory2 + 1 starter.tween_factory1 + 2 pyramid.tweens.excview_tween_factory + - MAIN + + Implicit Tween Chain (not used) + + Position Name + -------- ---- + - INGRESS + 0 pyramid_debugtoolbar.toolbar.toolbar_tween_factory + 1 pyramid.tweens.excview_tween_factory + - MAIN + +Here's the application configuration section of the ``development.ini`` used by +the above ``ptweens`` command which reports that the explicit tween chain is +used: + +.. code-block:: ini + :linenos: + + [app:main] + use = egg:starter + reload_templates = true + debug_authorization = false + debug_notfound = false + debug_routematch = false + debug_templates = true + default_locale_name = en + pyramid.include = pyramid_debugtoolbar + pyramid.tweens = starter.tween_factory2 + starter.tween_factory1 + pyramid.tweens.excview_tween_factory + +See :ref:`registering_tweens` for more information about tweens. + + +.. index:: + single: invoking a request + single: prequest + +.. _invoking_a_request: + +Invoking a Request +------------------ + +.. seealso:: See also the output of :ref:`prequest --help <prequest_script>`. + +You can use the ``prequest`` command-line utility to send a request to your +application and see the response body without starting a server. + +There are two required arguments to ``prequest``: + +- The config file/section: follows the format ``config_file#section_name``, + where ``config_file`` is the path to your application's ``.ini`` file and + ``section_name`` is the ``app`` section name inside the ``.ini`` file. The + ``section_name`` is optional; it defaults to ``main``. For example: + ``development.ini``. + +- The path: this should be the non-URL-quoted path element of the URL to the + resource you'd like to be rendered on the server. For example, ``/``. + +For example:: + + $ $VENV/bin/prequest development.ini / + +This will print the body of the response to the console on which it was +invoked. + +Several options are supported by ``prequest``. These should precede any config +file name or URL. + +``prequest`` has a ``-d`` (i.e., ``--display-headers``) option which prints the +status and headers returned by the server before the output:: + + $ $VENV/bin/prequest -d development.ini / + +This will print the status, headers, and the body of the response to the +console. + +You can add request header values by using the ``--header`` option:: + + $ $VENV/bin/prequest --header=Host:example.com development.ini / + +Headers are added to the WSGI environment by converting them to their CGI/WSGI +equivalents (e.g., ``Host=example.com`` will insert the ``HTTP_HOST`` header +variable as the value ``example.com``). Multiple ``--header`` options can be +supplied. The special header value ``content-type`` sets the ``CONTENT_TYPE`` +in the WSGI environment. + +By default, ``prequest`` sends a ``GET`` request. You can change this by using +the ``-m`` (aka ``--method``) option. ``GET``, ``HEAD``, ``POST``, and +``DELETE`` are currently supported. When you use ``POST``, the standard input +of the ``prequest`` process is used as the ``POST`` body:: + + $ $VENV/bin/prequest -mPOST development.ini / < somefile + + +Using Custom Arguments to Python when Running ``p*`` Scripts +------------------------------------------------------------ + +.. versionadded:: 1.5 + +Each of Pyramid's console scripts (``pserve``, ``pviews``, etc.) can be run +directly using ``python3 -m``, allowing custom arguments to be sent to the +Python interpreter at runtime. For example:: + + python3 -m pyramid.scripts.pserve development.ini + + +.. index:: + single: pdistreport + single: distributions, showing installed + single: showing installed distributions + +.. _showing_distributions: + +Showing All Installed Distributions and Their Versions +------------------------------------------------------ + +.. versionadded:: 1.5 + +.. seealso:: See also the output of :ref:`pdistreport --help + <pdistreport_script>`. + +You can use the ``pdistreport`` command to show the :app:`Pyramid` version in +use, the Python version in use, and all installed versions of Python +distributions in your Python environment:: + + $ $VENV/bin/pdistreport + Pyramid version: 1.5dev + Platform Linux-3.2.0-51-generic-x86_64-with-debian-wheezy-sid + Packages: + authapp 0.0 + /home/chrism/projects/foo/src/authapp + beautifulsoup4 4.1.3 + /home/chrism/projects/foo/lib/python2.7/site-packages/beautifulsoup4-4.1.3-py2.7.egg + ... more output ... + +``pdistreport`` takes no options. Its output is useful to paste into a +pastebin when you are having problems and need someone with more familiarity +with Python packaging and distribution than you have to look at your +environment. + + +.. _writing_a_script: + +Writing a Script +---------------- + +All web applications are, at their hearts, systems which accept a request and +return a response. When a request is accepted by a :app:`Pyramid` application, +the system receives state from the request which is later relied on by your +application code. For example, one :term:`view callable` may assume it's +working against a request that has a ``request.matchdict`` of a particular +composition, while another assumes a different composition of the matchdict. + +In the meantime, it's convenient to be able to write a Python script that can +work "in a Pyramid environment", for instance to update database tables used by +your :app:`Pyramid` application. But a "real" Pyramid environment doesn't have +a completely static state independent of a request; your application (and +Pyramid itself) is almost always reliant on being able to obtain information +from a request. When you run a Python script that simply imports code from +your application and tries to run it, there just is no request data, because +there isn't any real web request. Therefore some parts of your application and +some Pyramid APIs will not work. + +For this reason, :app:`Pyramid` makes it possible to run a script in an +environment much like the environment produced when a particular +:term:`request` reaches your :app:`Pyramid` application. This is achieved by +using the :func:`pyramid.paster.bootstrap` command in the body of your script. + +.. versionadded:: 1.1 + :func:`pyramid.paster.bootstrap` + +In the simplest case, :func:`pyramid.paster.bootstrap` can be used with a +single argument, which accepts the :term:`PasteDeploy` ``.ini`` file +representing your Pyramid application's configuration as a single argument: + +.. code-block:: python + + from pyramid.paster import bootstrap + env = bootstrap('/path/to/my/development.ini') + print(env['request'].route_url('home')) + +:func:`pyramid.paster.bootstrap` returns a dictionary containing +framework-related information. This dictionary will always contain a +:term:`request` object as its ``request`` key. + +The following keys are available in the ``env`` dictionary returned by +:func:`pyramid.paster.bootstrap`: + +request + + A :class:`pyramid.request.Request` object implying the current request + state for your script. + +app + + The :term:`WSGI` application object generated by bootstrapping. + +root + + The :term:`resource` root of your :app:`Pyramid` application. This is an + object generated by the :term:`root factory` configured in your + application. + +registry + + The :term:`application registry` of your :app:`Pyramid` application. + +closer + + A parameterless callable that can be used to pop an internal :app:`Pyramid` + threadlocal stack (used by :func:`pyramid.threadlocal.get_current_registry` + and :func:`pyramid.threadlocal.get_current_request`) when your scripting + job is finished. + +Let's assume that the ``/path/to/my/development.ini`` file used in the example +above looks like so: + +.. code-block:: ini + + [pipeline:main] + pipeline = translogger + another + + [filter:translogger] + filter_app_factory = egg:Paste#translogger + setup_console_handler = False + logger_name = wsgi + + [app:another] + use = egg:MyProject + +The configuration loaded by the above bootstrap example will use the +configuration implied by the ``[pipeline:main]`` section of your configuration +file by default. Specifying ``/path/to/my/development.ini`` is logically +equivalent to specifying ``/path/to/my/development.ini#main``. In this case, +we'll be using a configuration that includes an ``app`` object which is wrapped +in the Paste "translogger" :term:`middleware` (which logs requests to the +console). + +You can also specify a particular *section* of the PasteDeploy ``.ini`` file to +load instead of ``main``: + +.. code-block:: python + + from pyramid.paster import bootstrap + env = bootstrap('/path/to/my/development.ini#another') + print(env['request'].route_url('home')) + +The above example specifies the ``another`` ``app``, ``pipeline``, or +``composite`` section of your PasteDeploy configuration file. The ``app`` +object present in the ``env`` dictionary returned by +:func:`pyramid.paster.bootstrap` will be a :app:`Pyramid` :term:`router`. + + +Changing the Request +~~~~~~~~~~~~~~~~~~~~ + +By default, Pyramid will generate a request object in the ``env`` dictionary +for the URL ``http://localhost:80/``. This means that any URLs generated by +Pyramid during the execution of your script will be anchored here. This is +generally not what you want. + +So how do we make Pyramid generate the correct URLs? + +Assuming that you have a route configured in your application like so: + +.. code-block:: python + + config.add_route('verify', '/verify/{code}') + +You need to inform the Pyramid environment that the WSGI application is +handling requests from a certain base. For example, we want to simulate +mounting our application at `https://example.com/prefix`, to ensure that the +generated URLs are correct for our deployment. This can be done by either +mutating the resulting request object, or more simply by constructing the +desired request and passing it into :func:`~pyramid.paster.bootstrap`: + +.. code-block:: python + + from pyramid.paster import bootstrap + from pyramid.request import Request + + request = Request.blank('/', base_url='https://example.com/prefix') + env = bootstrap('/path/to/my/development.ini#another', request=request) + print(env['request'].application_url) + # will print 'https://example.com/prefix' + +Now you can readily use Pyramid's APIs for generating URLs: + +.. code-block:: python + + env['request'].route_url('verify', code='1337') + # will return 'https://example.com/prefix/verify/1337' + + +Cleanup +~~~~~~~ + +When your scripting logic finishes, it's good manners to call the ``closer`` +callback: + +.. code-block:: python + + from pyramid.paster import bootstrap + env = bootstrap('/path/to/my/development.ini') + + # .. do stuff ... + + env['closer']() + + +Setting Up Logging +~~~~~~~~~~~~~~~~~~ + +By default, :func:`pyramid.paster.bootstrap` does not configure logging +parameters present in the configuration file. If you'd like to configure +logging based on ``[logger]`` and related sections in the configuration file, +use the following command: + +.. code-block:: python + + import pyramid.paster + pyramid.paster.setup_logging('/path/to/my/development.ini') + +See :ref:`logging_chapter` for more information on logging within +:app:`Pyramid`. + + +.. index:: + single: console script + +.. _making_a_console_script: + +Making Your Script into a Console Script +---------------------------------------- + +A "console script" is :term:`setuptools` terminology for a script that gets +installed into the ``bin`` directory of a Python :term:`virtual environment` +(or "base" Python environment) when a :term:`distribution` which houses that +script is installed. Because it's installed into the ``bin`` directory of a +virtual environment when the distribution is installed, it's a convenient way +to package and distribute functionality that you can call from the +command-line. It's often more convenient to create a console script than it is +to create a ``.py`` script and instruct people to call it with the "right" +Python interpreter. A console script generates a file that lives in ``bin``, +and when it's invoked it will always use the "right" Python environment, which +means it will always be invoked in an environment where all the libraries it +needs (such as Pyramid) are available. + +In general, you can make your script into a console script by doing the +following: + +- Use an existing distribution (such as one you've already created via + ``pcreate``) or create a new distribution that possesses at least one package + or module. It should, within any module within the distribution, house a + callable (usually a function) that takes no arguments and which runs any of + the code you wish to run. + +- Add a ``[console_scripts]`` section to the ``entry_points`` argument of the + distribution which creates a mapping between a script name and a dotted name + representing the callable you added to your distribution. + +- Run ``pip install -e .`` or ``pip install .`` to get your distribution + reinstalled. When you reinstall your distribution, a file representing the + script that you named in the last step will be in the ``bin`` directory of + the virtual environment in which you installed the distribution. It will be + executable. Invoking it from a terminal will execute your callable. + +As an example, let's create some code that can be invoked by a console script +that prints the deployment settings of a Pyramid application. To do so, we'll +pretend you have a distribution with a package in it named ``myproject``. +Within this package, we'll pretend you've added a ``scripts.py`` module which +contains the following code: + +.. code-block:: python + :linenos: + + # myproject.scripts module + + import optparse + import sys + import textwrap + + from pyramid.paster import bootstrap + + def settings_show(): + description = """\ + Print the deployment settings for a Pyramid application. Example: + 'show_settings deployment.ini' + """ + usage = "usage: %prog config_uri" + parser = optparse.OptionParser( + usage=usage, + description=textwrap.dedent(description) + ) + parser.add_option( + '-o', '--omit', + dest='omit', + metavar='PREFIX', + type='string', + action='append', + help=("Omit settings which start with PREFIX (you can use this " + "option multiple times)") + ) + + options, args = parser.parse_args(sys.argv[1:]) + if not len(args) >= 1: + print('You must provide at least one argument') + return 2 + config_uri = args[0] + omit = options.omit + if omit is None: + omit = [] + env = bootstrap(config_uri) + settings, closer = env['registry'].settings, env['closer'] + try: + for k, v in settings.items(): + if any([k.startswith(x) for x in omit]): + continue + print('%-40s %-20s' % (k, v)) + finally: + closer() + +This script uses the Python ``optparse`` module to allow us to make sense out +of extra arguments passed to the script. It uses the +:func:`pyramid.paster.bootstrap` function to get information about the +application defined by a config file, and prints the deployment settings +defined in that config file. + +After adding this script to the package, you'll need to tell your +distribution's ``setup.py`` about its existence. Within your distribution's +top-level directory, your ``setup.py`` file will look something like this: + +.. code-block:: python + :linenos: + + import os + + from setuptools import setup, find_packages + + here = os.path.abspath(os.path.dirname(__file__)) + with open(os.path.join(here, 'README.txt')) as f: + README = f.read() + with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + + requires = ['pyramid', 'pyramid_debugtoolbar'] + + tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + + setup(name='MyProject', + version='0.0', + description='My project', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + extras_require={ + 'testing': tests_require, + }, + entry_points = """\ + [paste.app_factory] + main = myproject:main + """, + ) + +We're going to change the ``setup.py`` file to add a ``[console_scripts]`` +section within the ``entry_points`` string. Within this section, you should +specify a ``scriptname = dotted.path.to:yourfunction`` line. For example: + +.. code-block:: ini + + [console_scripts] + show_settings = myproject.scripts:settings_show + +The ``show_settings`` name will be the name of the script that is installed +into ``bin``. The colon (``:``) between ``myproject.scripts`` and +``settings_show`` above indicates that ``myproject.scripts`` is a Python +module, and ``settings_show`` is the function in that module which contains the +code you'd like to run as the result of someone invoking the ``show_settings`` +script from their command line. + +The result will be something like: + +.. code-block:: python + :linenos: + :emphasize-lines: 43-44 + + import os + + from setuptools import setup, find_packages + + here = os.path.abspath(os.path.dirname(__file__)) + with open(os.path.join(here, 'README.txt')) as f: + README = f.read() + with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + + requires = ['pyramid', 'pyramid_debugtoolbar'] + + tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + + setup(name='MyProject', + version='0.0', + description='My project', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + extras_require={ + 'testing': tests_require, + }, + entry_points = """\ + [paste.app_factory] + main = myproject:main + [console_scripts] + show_settings = myproject.scripts:settings_show + """, + ) + +Once you've done this, invoking ``$VENV/bin/pip install -e .`` will install a +file named ``show_settings`` into the ``$somevenv/bin`` directory with a +small bit of Python code that points to your entry point. It will be +executable. Running it without any arguments will print an error and exit. +Running it with a single argument that is the path of a config file will print +the settings. Running it with an ``--omit=foo`` argument will omit the settings +that have keys that start with ``foo``. Running it with two "omit" options +(e.g., ``--omit=foo --omit=bar``) will omit all settings that have keys that +start with either ``foo`` or ``bar``: + +.. code-block:: bash + + $ $VENV/bin/show_settings development.ini --omit=pyramid --omit=debugtoolbar + debug_routematch False + debug_templates True + reload_templates True + mako.directories [] + debug_notfound False + default_locale_name en + reload_resources False + debug_authorization False + reload_assets False + prevent_http_cache False + +Pyramid's ``pserve``, ``pcreate``, ``pshell``, ``prequest``, ``ptweens``, and +other ``p*`` scripts are implemented as console scripts. When you invoke one +of those, you are using a console script. diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst index 6360dc574..cde166b21 100644 --- a/docs/narr/configuration.rst +++ b/docs/narr/configuration.rst @@ -3,25 +3,26 @@ .. _configuration_narr: -Application Configuration +Application Configuration ========================= -Each deployment of an application written using :app:`Pyramid` implies a -specific *configuration* of the framework itself. For example, an -application which serves up MP3 files for your listening enjoyment might plug -code into the framework that manages song files, while an application that -manages corporate data might plug in code that manages accounting -information. The way in which code is plugged in to :app:`Pyramid` for a -specific application is referred to as "configuration". - -Most people understand "configuration" as coarse settings that inform the -high-level operation of a specific application deployment. For instance, -it's easy to think of the values implied by a ``.ini`` file parsed at -application startup time as "configuration". :app:`Pyramid` extends this -pattern to application development, using the term "configuration" to express -standardized ways that code gets plugged into a deployment of the framework -itself. When you plug code into the :app:`Pyramid` framework, you are -"configuring" :app:`Pyramid` to create a particular application. +Most people already understand "configuration" as settings that influence the +operation of an application. For instance, it's easy to think of the values in +a ``.ini`` file parsed at application startup time as "configuration". However, +if you're reasonably open-minded, it's easy to think of *code* as configuration +too. Since Pyramid, like most other web application platforms, is a +*framework*, it calls into code that you write (as opposed to a *library*, +which is code that exists purely for you to call). The act of plugging +application code that you've written into :app:`Pyramid` is also referred to +within this documentation as "configuration"; you are configuring +:app:`Pyramid` to call the code that makes up your application. + +.. seealso:: + For information on ``.ini`` files for Pyramid applications see the + :ref:`startup_chapter` chapter. + +There are two ways to configure a :app:`Pyramid` application: :term:`imperative +configuration` and :term:`declarative configuration`. Both are described below. .. index:: single: imperative configuration @@ -31,13 +32,14 @@ itself. When you plug code into the :app:`Pyramid` framework, you are Imperative Configuration ------------------------ -Here's one of the simplest :app:`Pyramid` applications, configured -imperatively: +"Imperative configuration" just means configuration done by Python statements, +one after the next. Here's one of the simplest :app:`Pyramid` applications, +configured imperatively: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -48,14 +50,15 @@ imperatively: config = Configurator() config.add_view(hello_world) app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() We won't talk much about what this application does yet. Just note that the "configuration' statements take place underneath the ``if __name__ == '__main__':`` stanza in the form of method calls on a :term:`Configurator` -object (e.g. ``config.add_view(...)``). These statements take place one -after the other, and are executed in order, so the full power of Python, -including conditionals, can be employed in this mode of configuration. +object (e.g., ``config.add_view(...)``). These statements take place one after +the other, and are executed in order, so the full power of Python, including +conditionals, can be employed in this mode of configuration. .. index:: single: view_config @@ -64,19 +67,17 @@ including conditionals, can be employed in this mode of configuration. .. _decorations_and_code_scanning: -Configuration Decorations and Code Scanning -------------------------------------------- +Declarative Configuration +------------------------- -A different mode of configuration gives more *locality of reference* to a -:term:`configuration declaration`. It's sometimes painful to have all -configuration done in imperative code, because often the code for a single -application may live in many files. If the configuration is centralized in -one place, you'll need to have at least two files open at once to see the -"big picture": the file that represents the configuration, and the file that -contains the implementation objects referenced by the configuration. To -avoid this, :app:`Pyramid` allows you to insert :term:`configuration -decoration` statements very close to code that is referred to by the -declaration itself. For example: +It's sometimes painful to have all configuration done by imperative code, +because often the code for a single application may live in many files. If the +configuration is centralized in one place, you'll need to have at least two +files open at once to see the "big picture": the file that represents the +configuration, and the file that contains the implementation objects referenced +by the configuration. To avoid this, :app:`Pyramid` allows you to insert +:term:`configuration decoration` statements very close to code that is referred +to by the declaration itself. For example: .. code-block:: python :linenos: @@ -88,70 +89,66 @@ declaration itself. For example: def hello(request): return Response('Hello') -The mere existence of configuration decoration doesn't cause any -configuration registration to be performed. Before it has any effect on the -configuration of a :app:`Pyramid` application, a configuration decoration -within application code must be found through a process known as a -:term:`scan`. +The mere existence of configuration decoration doesn't cause any configuration +registration to be performed. Before it has any effect on the configuration of +a :app:`Pyramid` application, a configuration decoration within application +code must be found through a process known as a :term:`scan`. For example, the :class:`pyramid.view.view_config` decorator in the code -example above adds an attribute to the ``hello`` function, making it -available for a :term:`scan` to find it later. - -A :term:`scan` of a :term:`module` or a :term:`package` and its subpackages -for decorations happens when the :meth:`pyramid.config.Configurator.scan` -method is invoked: scanning implies searching for configuration declarations -in a package and its subpackages. For example: - -.. topic:: Starting A Scan - - .. code-block:: python - :linenos: - - from paste.httpserver import serve - from pyramid.response import Response - from pyramid.view import view_config - - @view_config() - def hello(request): - return Response('Hello') - - if __name__ == '__main__': - from pyramid.config import Configurator - config = Configurator() - config.scan() - app = config.make_wsgi_app() - serve(app, host='0.0.0.0') +example above adds an attribute to the ``hello`` function, making it available +for a :term:`scan` to find it later. + +A :term:`scan` of a :term:`module` or a :term:`package` and its subpackages for +decorations happens when the :meth:`pyramid.config.Configurator.scan` method is +invoked: scanning implies searching for configuration declarations in a package +and its subpackages. For example: + +.. code-block:: python + :linenos: + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.response import Response + from pyramid.view import view_config + + @view_config() + def hello(request): + return Response('Hello') + + if __name__ == '__main__': + config = Configurator() + config.scan() + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() The scanning machinery imports each module and subpackage in a package or -module recursively, looking for special attributes attached to objects -defined within a module. These special attributes are typically attached to -code via the use of a :term:`decorator`. For example, the +module recursively, looking for special attributes attached to objects defined +within a module. These special attributes are typically attached to code via +the use of a :term:`decorator`. For example, the :class:`~pyramid.view.view_config` decorator can be attached to a function or instance method. -Once scanning is invoked, and :term:`configuration decoration` is found by -the scanner, a set of calls are made to a :term:`Configurator` on your -behalf: these calls replace the need to add imperative configuration -statements that don't live near the code being configured. +Once scanning is invoked, and :term:`configuration decoration` is found by the +scanner, a set of calls are made to a :term:`Configurator` on your behalf. +These calls replace the need to add imperative configuration statements that +don't live near the code being configured. + +The combination of :term:`configuration decoration` and the invocation of a +:term:`scan` is collectively known as :term:`declarative configuration`. In the example above, the scanner translates the arguments to :class:`~pyramid.view.view_config` into a call to the :meth:`pyramid.config.Configurator.add_view` method, effectively: -.. ignore-next-block .. code-block:: python - :linenos: config.add_view(hello) -Declarative Configuration -------------------------- - -A third mode of configuration can be employed when you create a -:app:`Pyramid` application named *declarative configuration*. This mode uses -an XML language known as :term:`ZCML` to represent configuration statements -rather than Python. ZCML is not built-in to Pyramid, but almost everything -that can be configured imperatively can also be configured via ZCML if you -install the :term:`pyramid_zcml` package. +Summary +------- +There are two ways to configure a :app:`Pyramid` application: declaratively and +imperatively. You can choose the mode with which you're most comfortable; both +are completely equivalent. Examples in this documentation will use both modes +interchangeably. diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 3b938c09c..743266d2c 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -8,6 +8,8 @@ single: debug_all single: reload_all single: debug settings + single: debug_routematch + single: prevent_http_cache single: reload settings single: default_locale_name single: environment variables @@ -19,330 +21,424 @@ Environment Variables and ``.ini`` File Settings ================================================ -:app:`Pyramid` behavior can be configured through a combination of -operating system environment variables and ``.ini`` configuration file -application section settings. The meaning of the environment -variables and the configuration file settings overlap. +:app:`Pyramid` behavior can be configured through a combination of operating +system environment variables and ``.ini`` configuration file application +section settings. The meaning of the environment variables and the +configuration file settings overlap. -.. note:: Where a configuration file setting exists with the same - meaning as an environment variable, and both are present at - application startup time, the environment variable setting - takes precedence. +.. note:: + Where a configuration file setting exists with the same meaning as an + environment variable, and both are present at application startup time, the + environment variable setting takes precedence. -The term "configuration file setting name" refers to a key in the -``.ini`` configuration for your application. The configuration file -setting names documented in this chapter are reserved for -:app:`Pyramid` use. You should not use them to indicate -application-specific configuration settings. +The term "configuration file setting name" refers to a key in the ``.ini`` +configuration for your application. The configuration file setting names +documented in this chapter are reserved for :app:`Pyramid` use. You should not +use them to indicate application-specific configuration settings. Reloading Templates ------------------- -When this value is true, templates are automatically reloaded whenever -they are modified without restarting the application, so you can see -changes to templates take effect immediately during development. This -flag is meaningful to Chameleon and Mako templates, as well as most -third-party template rendering extensions. - -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_RELOAD_TEMPLATES`` | ``reload_templates`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ +When this value is true, templates are automatically reloaded whenever they are +modified without restarting the application, so you can see changes to +templates take effect immediately during development. This flag is meaningful +to Chameleon and Mako templates, as well as most third-party template rendering +extensions. + ++-------------------------------+--------------------------------+ +| Environment Variable Name | Config File Setting Name | ++===============================+================================+ +| ``PYRAMID_RELOAD_TEMPLATES`` | ``pyramid.reload_templates`` | +| | or ``reload_templates`` | ++-------------------------------+--------------------------------+ Reloading Assets ---------------- -Don't cache any asset file data when this value is true. See -also :ref:`overriding_assets_section`. +Don't cache any asset file data when this value is true. + +.. seealso:: + + See also :ref:`overriding_assets_section`. -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_RELOAD_ASSETS`` | ``reload_assets`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ ++----------------------------+-----------------------------+ +| Environment Variable Name | Config File Setting Name | ++============================+=============================+ +| ``PYRAMID_RELOAD_ASSETS`` | ``pyramid.reload_assets`` | +| | or ``reload_assets`` | ++----------------------------+-----------------------------+ -.. note:: For backwards compatibility purposes, aliases can be - used for configurating asset reloading: ``PYRAMID_RELOAD_RESOURCES`` (envvar) - and ``reload_resources`` (config file). +.. note:: For backwards compatibility purposes, aliases can be used for + configuring asset reloading: ``PYRAMID_RELOAD_RESOURCES`` (envvar) and + ``pyramid.reload_resources`` (config file). Debugging Authorization ----------------------- -Print view authorization failure and success information to stderr -when this value is true. See also :ref:`debug_authorization_section`. +Print view authorization failure and success information to stderr when this +value is true. -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_DEBUG_AUTHORIZATION`` | ``debug_authorization`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ +.. seealso:: + + See also :ref:`debug_authorization_section`. + ++---------------------------------+-----------------------------------+ +| Environment Variable Name | Config File Setting Name | ++=================================+===================================+ +| ``PYRAMID_DEBUG_AUTHORIZATION`` | ``pyramid.debug_authorization`` | +| | or ``debug_authorization`` | ++---------------------------------+-----------------------------------+ Debugging Not Found Errors -------------------------- -Print view-related ``NotFound`` debug messages to stderr -when this value is true. See also :ref:`debug_notfound_section`. +Print view-related ``NotFound`` debug messages to stderr when this value is +true. + +.. seealso:: -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_DEBUG_NOTFOUND`` | ``debug_notfound`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ + See also :ref:`debug_notfound_section`. + ++----------------------------+------------------------------+ +| Environment Variable Name | Config File Setting Name | ++============================+==============================+ +| ``PYRAMID_DEBUG_NOTFOUND`` | ``pyramid.debug_notfound`` | +| | or ``debug_notfound`` | ++----------------------------+------------------------------+ Debugging Route Matching ------------------------ Print debugging messages related to :term:`url dispatch` route matching when -this value is true. See also :ref:`debug_routematch_section`. +this value is true. + +.. seealso:: + + See also :ref:`debug_routematch_section`. -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_DEBUG_ROUTEMATCH`` | ``debug_routematch`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ ++------------------------------+--------------------------------+ +| Environment Variable Name | Config File Setting Name | ++==============================+================================+ +| ``PYRAMID_DEBUG_ROUTEMATCH`` | ``pyramid.debug_routematch`` | +| | or ``debug_routematch`` | ++------------------------------+--------------------------------+ + +.. _preventing_http_caching: + +Preventing HTTP Caching +----------------------- + +Prevent the ``http_cache`` view configuration argument from having any effect +globally in this process when this value is true. No HTTP caching-related +response headers will be set by the :app:`Pyramid` ``http_cache`` view +configuration feature when this is true. + +.. seealso:: + + See also :ref:`influencing_http_caching`. + ++---------------------------------+----------------------------------+ +| Environment Variable Name | Config File Setting Name | ++=================================+==================================+ +| ``PYRAMID_PREVENT_HTTP_CACHE`` | ``pyramid.prevent_http_cache`` | +| | or ``prevent_http_cache`` | ++---------------------------------+----------------------------------+ + +Preventing Cache Busting +------------------------ + +Prevent the ``cachebust`` static view configuration argument from having any +effect globally in this process when this value is true. No cache buster will +be configured or used when this is true. + +.. versionadded:: 1.6 + +.. seealso:: + + See also :ref:`cache_busting`. + ++---------------------------------+----------------------------------+ +| Environment Variable Name | Config File Setting Name | ++=================================+==================================+ +| ``PYRAMID_PREVENT_CACHEBUST`` | ``pyramid.prevent_cachebust`` | +| | or ``prevent_cachebust`` | ++---------------------------------+----------------------------------+ Debugging All ------------- Turns on all ``debug*`` settings. -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_DEBUG_ALL`` | ``debug_all`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ ++----------------------------+---------------------------+ +| Environment Variable Name | Config File Setting Name | ++============================+===========================+ +| ``PYRAMID_DEBUG_ALL`` | ``pyramid.debug_all`` | +| | or ``debug_all`` | ++----------------------------+---------------------------+ Reloading All ------------- Turns on all ``reload*`` settings. -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_RELOAD_ALL`` | ``reload_all`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ ++---------------------------+----------------------------+ +| Environment Variable Name | Config File Setting Name | ++===========================+============================+ +| ``PYRAMID_RELOAD_ALL`` | ``pyramid.reload_all`` or | +| | ``reload_all`` | ++---------------------------+----------------------------+ .. _default_locale_name_setting: Default Locale Name --------------------- - -The value supplied here is used as the default locale name when a -:term:`locale negotiator` is not registered. See also -:ref:`localization_deployment_settings`. - -+---------------------------------+-----------------------------+ -| Environment Variable Name | Config File Setting Name | -+=================================+=============================+ -| ``PYRAMID_DEFAULT_LOCALE_NAME`` | ``default_locale_name`` | -| | | -| | | -| | | -+---------------------------------+-----------------------------+ - -.. _mako_template_renderer_settings: - -Mako Template Render Settings ------------------------------ - -Mako derives additional settings to configure its template renderer that -should be set when using it. Many of these settings are optional and only need -to be set if they should be different from the default. The Mako Template -Renderer uses a subclass of Mako's `template lookup -<http://www.makotemplates.org/docs/usage.html#usage_lookup>`_ and accepts -several arguments to configure it. - -Mako Directories -++++++++++++++++ - -The value(s) supplied here are passed in as the template directories. They -should be in :term:`asset specification` format, for example: -``my.package:templates``. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.directories`` | -| | -| | -| | -+-----------------------------+ - -Mako Module Directory -+++++++++++++++++++++ - -The value supplied here tells Mako where to store compiled Mako templates. If -omitted, compiled templates will be stored in memory. This value should be an -absolute path, for example: ``%(here)s/data/templates`` would use a directory -called ``data/templates`` in the same parent directory as the INI file. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.module_directory`` | -| | -| | -| | -+-----------------------------+ - -Mako Input Encoding -+++++++++++++++++++ - -The encoding that Mako templates are assumed to have. By default this is set -to ``utf-8``. If you wish to use a different template encoding, this value -should be changed accordingly. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.input_encoding`` | -| | -| | -| | -+-----------------------------+ - -Mako Error Handler -++++++++++++++++++ - -Python callable which is called whenever Mako compile or runtime exceptions -occur. The callable is passed the current context as well as the exception. If -the callable returns True, the exception is considered to be handled, else it -is re-raised after the function completes. Is used to provide custom -error-rendering functions. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.error_handler`` | -| | -| | -| | -+-----------------------------+ - -Mako Default Filters -++++++++++++++++++++ - -List of string filter names that will be applied to all Mako expressions. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.default_filters`` | -| | -| | -| | -+-----------------------------+ - -Mako Import -+++++++++++ - -String list of Python statements, typically individual "import" lines, which -will be placed into the module level preamble of all generated Python modules. - - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.imports`` | -| | -| | -| | -+-----------------------------+ - - -Mako Strict Undefined -+++++++++++++++++++++ - -``true`` or ``false``, representing the "strict undefined" behavior of Mako -(see `Mako Context Variables -<http://www.makotemplates.org/docs/runtime.html#context-variables>`_). By -default, this is ``false``. - -+-----------------------------+ -| Config File Setting Name | -+=============================+ -| ``mako.strict_undefined`` | -| | -| | -| | -+-----------------------------+ +------------------- + +The value supplied here is used as the default locale name when a :term:`locale +negotiator` is not registered. + +.. seealso:: + + See also :ref:`localization_deployment_settings`. + ++---------------------------------+-----------------------------------+ +| Environment Variable Name | Config File Setting Name | ++=================================+===================================+ +| ``PYRAMID_DEFAULT_LOCALE_NAME`` | ``pyramid.default_locale_name`` | +| | or ``default_locale_name`` | ++---------------------------------+-----------------------------------+ + +.. _including_packages: + +Including Packages +------------------ + +``pyramid.includes`` instructs your application to include other packages. +Using the setting is equivalent to using the +:meth:`pyramid.config.Configurator.include` method. + ++--------------------------+ +| Config File Setting Name | ++==========================+ +| ``pyramid.includes`` | ++--------------------------+ + +The value assigned to ``pyramid.includes`` should be a sequence. The sequence +can take several different forms. + +1) It can be a string. + + If it is a string, the package names can be separated by spaces:: + + package1 package2 package3 + + The package names can also be separated by carriage returns:: + + package1 + package2 + package3 + +2) It can be a Python list, where the values are strings:: + + ['package1', 'package2', 'package3'] + +Each value in the sequence should be a :term:`dotted Python name`. + +``pyramid.includes`` vs. :meth:`pyramid.config.Configurator.include` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two methods exist for including packages: ``pyramid.includes`` and +:meth:`pyramid.config.Configurator.include`. This section explains their +equivalence. + +Using PasteDeploy ++++++++++++++++++ + +Using the following ``pyramid.includes`` setting in the PasteDeploy ``.ini`` +file in your application: + +.. code-block:: ini + + [app:main] + pyramid.includes = pyramid_debugtoolbar + pyramid_tm + +Is equivalent to using the following statements in your configuration code: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator(settings=settings) + # ... + config.include('pyramid_debugtoolbar') + config.include('pyramid_tm') + # ... + +It is fine to use both or either form. + +Plain Python +++++++++++++ + +Using the following ``pyramid.includes`` setting in your plain-Python Pyramid +application: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + settings = {'pyramid.includes':'pyramid_debugtoolbar pyramid_tm'} + config = Configurator(settings=settings) + +Is equivalent to using the following statements in your configuration code: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + settings = {} + config = Configurator(settings=settings) + config.include('pyramid_debugtoolbar') + config.include('pyramid_tm') + +It is fine to use both or either form. + +.. _explicit_tween_config: + +Explicit Tween Configuration +---------------------------- + +This value allows you to perform explicit :term:`tween` ordering in your +configuration. Tweens are bits of code used by add-on authors to extend +Pyramid. They form a chain, and require ordering. + +Ideally you won't need to use the ``pyramid.tweens`` setting at all. Tweens +are generally ordered and included "implicitly" when an add-on package which +registers a tween is "included". Packages are included when you name a +``pyramid.includes`` setting in your configuration or when you call +:meth:`pyramid.config.Configurator.include`. + +Authors of included add-ons provide "implicit" tween configuration ordering +hints to Pyramid when their packages are included. However, the implicit tween +ordering is only best-effort. Pyramid will attempt to provide an implicit +order of tweens as best it can using hints provided by add-on authors, but +because it's only best-effort, if very precise tween ordering is required, the +only surefire way to get it is to use an explicit tween order. You may be +required to inspect your tween ordering (see :ref:`displaying_tweens`) and add +a ``pyramid.tweens`` configuration value at the behest of an add-on author. + ++---------------------------+ +| Config File Setting Name | ++===========================+ +| ``pyramid.tweens`` | ++---------------------------+ + +The value assigned to ``pyramid.tweens`` should be a sequence. The sequence +can take several different forms. + +1) It can be a string. + + If it is a string, the tween names can be separated by spaces:: + + pkg.tween_factory1 pkg.tween_factory2 pkg.tween_factory3 + + The tween names can also be separated by carriage returns:: + + pkg.tween_factory1 + pkg.tween_factory2 + pkg.tween_factory3 + +2) It can be a Python list, where the values are strings:: + + ['pkg.tween_factory1', 'pkg.tween_factory2', 'pkg.tween_factory3'] + +Each value in the sequence should be a :term:`dotted Python name`. + +PasteDeploy Configuration vs. Plain-Python Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using the following ``pyramid.tweens`` setting in the PasteDeploy ``.ini`` file +in your application: + +.. code-block:: ini + + [app:main] + pyramid.tweens = pyramid_debugtoolbar.toolbar.tween_factory + pyramid.tweens.excview_tween_factory + pyramid_tm.tm_tween_factory + +Is equivalent to using the following statements in your configuration code: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + settings['pyramid.tweens'] = [ + 'pyramid_debugtoolbar.toolbar.tween_factory', + 'pyramid.tweebs.excview_tween_factory', + 'pyramid_tm.tm_tween_factory', + ] + config = Configurator(settings=settings) + +It is fine to use both or either form. Examples -------- -Let's presume your configuration file is named ``MyProject.ini``, and -there is a section representing your application named ``[app:main]`` -within the file that represents your :app:`Pyramid` application. -The configuration file settings documented in the above "Config File -Setting Name" column would go in the ``[app:main]`` section. Here's -an example of such a section: +Let's presume your configuration file is named ``MyProject.ini``, and there is +a section representing your application named ``[app:main]`` within the file +that represents your :app:`Pyramid` application. The configuration file +settings documented in the above "Config File Setting Name" column would go in +the ``[app:main]`` section. Here's an example of such a section: .. code-block:: ini :linenos: [app:main] - use = egg:MyProject#app - reload_templates = true - debug_authorization = true + use = egg:MyProject + pyramid.reload_templates = true + pyramid.debug_authorization = true -You can also use environment variables to accomplish the same purpose -for settings documented as such. For example, you might start your -:app:`Pyramid` application using the following command line: +You can also use environment variables to accomplish the same purpose for +settings documented as such. For example, you might start your :app:`Pyramid` +application using the following command line: .. code-block:: text $ PYRAMID_DEBUG_AUTHORIZATION=1 PYRAMID_RELOAD_TEMPLATES=1 \ - bin/paster serve MyProject.ini - -If you started your application this way, your :app:`Pyramid` -application would behave in the same manner as if you had placed the -respective settings in the ``[app:main]`` section of your -application's ``.ini`` file. - -If you want to turn all ``debug`` settings (every setting that starts -with ``debug_``). on in one fell swoop, you can use -``PYRAMID_DEBUG_ALL=1`` as an environment variable setting or you may use -``debug_all=true`` in the config file. Note that this does not affect -settings that do not start with ``debug_*`` such as -``reload_templates``. - -If you want to turn all ``reload`` settings (every setting that starts -with ``reload_``) on in one fell swoop, you can use + $VENV/bin/pserve MyProject.ini + +If you started your application this way, your :app:`Pyramid` application would +behave in the same manner as if you had placed the respective settings in the +``[app:main]`` section of your application's ``.ini`` file. + +If you want to turn all ``debug`` settings (every setting that starts with +``pyramid.debug_``) on in one fell swoop, you can use ``PYRAMID_DEBUG_ALL=1`` +as an environment variable setting or you may use ``pyramid.debug_all=true`` in +the config file. Note that this does not affect settings that do not start +with ``pyramid.debug_*`` such as ``pyramid.reload_templates``. + +If you want to turn all ``pyramid.reload`` settings (every setting that starts +with ``pyramid.reload_``) on in one fell swoop, you can use ``PYRAMID_RELOAD_ALL=1`` as an environment variable setting or you may use -``reload_all=true`` in the config file. Note that this does not -affect settings that do not start with ``reload_*`` such as -``debug_notfound``. +``pyramid.reload_all=true`` in the config file. Note that this does not affect +settings that do not start with ``pyramid.reload_*`` such as +``pyramid.debug_notfound``. .. note:: Specifying configuration settings via environment variables is generally - most useful during development, where you may wish to augment or - override the more permanent settings in the configuration file. - This is useful because many of the reload and debug settings may - have performance or security (i.e., disclosure) implications - that make them undesirable in a production environment. + most useful during development, where you may wish to augment or override + the more permanent settings in the configuration file. This is useful + because many of the reload and debug settings may have performance or + security (i.e., disclosure) implications that make them undesirable in a + production environment. .. index:: single: reload_templates @@ -351,39 +447,42 @@ affect settings that do not start with ``reload_*`` such as Understanding the Distinction Between ``reload_templates`` and ``reload_assets`` -------------------------------------------------------------------------------- -The difference between ``reload_assets`` and ``reload_templates`` is a bit -subtle. Templates are themselves also treated by :app:`Pyramid` as asset -files (along with other static files), so the distinction can be confusing. -It's helpful to read :ref:`overriding_assets_section` for some context -about assets in general. +The difference between ``pyramid.reload_assets`` and +``pyramid.reload_templates`` is a bit subtle. Templates are themselves also +treated by :app:`Pyramid` as asset files (along with other static files), so +the distinction can be confusing. It's helpful to read +:ref:`overriding_assets_section` for some context about assets in general. -When ``reload_templates`` is true, :app:`Pyramid` takes advantage of the -underlying templating systems' ability to check for file modifications to an -individual template file. When ``reload_templates`` is true but -``reload_assets`` is *not* true, the template filename returned by the +When ``pyramid.reload_templates`` is true, :app:`Pyramid` takes advantage of +the underlying templating system's ability to check for file modifications to +an individual template file. When ``pyramid.reload_templates`` is true, but +``pyramid.reload_assets`` is *not* true, the template filename returned by the ``pkg_resources`` package (used under the hood by asset resolution) is cached by :app:`Pyramid` on the first request. Subsequent requests for the same template file will return a cached template filename. The underlying templating system checks for modifications to this particular file for every -request. Setting ``reload_templates`` to ``True`` doesn't affect performance -dramatically (although it should still not be used in production because it -has some effect). - -However, when ``reload_assets`` is true, :app:`Pyramid` will not cache the -template filename, meaning you can see the effect of changing the content of -an overridden asset directory for templates without restarting the server -after every change. Subsequent requests for the same template file may -return different filenames based on the current state of overridden asset -directories. Setting ``reload_assets`` to ``True`` affects performance -*dramatically*, slowing things down by an order of magnitude for each -template rendering. However, it's convenient to enable when moving files -around in overridden asset directories. ``reload_assets`` makes the system -*very slow* when templates are in use. Never set ``reload_assets`` to +request. Setting ``pyramid.reload_templates`` to ``True`` doesn't affect +performance dramatically (although it should still not be used in production +because it has some effect). + +However, when ``pyramid.reload_assets`` is true, :app:`Pyramid` will not cache +the template filename, meaning you can see the effect of changing the content +of an overridden asset directory for templates without restarting the server +after every change. Subsequent requests for the same template file may return +different filenames based on the current state of overridden asset directories. +Setting ``pyramid.reload_assets`` to ``True`` affects performance +*dramatically*, slowing things down by an order of magnitude for each template +rendering. However, it's convenient to enable when moving files around in +overridden asset directories. ``pyramid.reload_assets`` makes the system *very +slow* when templates are in use. Never set ``pyramid.reload_assets`` to ``True`` on a production system. +.. index:: + par: settings; adding custom + .. _adding_a_custom_setting: -Adding A Custom Setting +Adding a Custom Setting ----------------------- From time to time, you may need to add a custom setting to your application. @@ -395,17 +494,17 @@ Here's how: .. code-block:: ini - [app:myapp] + [app:main] # .. other settings debug_frobnosticator = True - In the ``main()`` function that represents the place that your Pyramid WSGI - application is created, anticipate that you'll be getting this key/value - pair as a setting and do any type conversion necessary. + application is created, anticipate that you'll be getting this key/value pair + as a setting and do any type conversion necessary. - If you've done any type conversion of your custom value, reset the - converted values into the ``settings`` dictionary *before* you pass the - dictionary as ``settings`` to the :term:`Configurator`. For example: + If you've done any type conversion of your custom value, reset the converted + values into the ``settings`` dictionary *before* you pass the dictionary as + ``settings`` to the :term:`Configurator`. For example: .. code-block:: python @@ -417,23 +516,35 @@ Here's how: settings['debug_frobnosticator'] = debug_frobnosticator config = Configurator(settings=settings) - .. note:: It's especially important that you mutate the ``settings`` - dictionary with the converted version of the variable *before* passing - it to the Configurator: the configurator makes a *copy* of ``settings``, - it doesn't use the one you pass directly. + .. note:: + It's especially important that you mutate the ``settings`` dictionary with + the converted version of the variable *before* passing it to the + Configurator: the configurator makes a *copy* of ``settings``, it doesn't + use the one you pass directly. + +- When creating an ``includeme`` function that will be later added to your + application's configuration you may access the ``settings`` dictionary + through the instance of the :term:`Configurator` that is passed into the + function as its only argument. For Example: -- In the runtime code that you need to access the new settings value, find - the value in the ``registry.settings`` dictionary and use it. In + .. code-block:: python + + def includeme(config): + settings = config.registry.settings + debug_frobnosticator = settings['debug_frobnosticator'] + +- In the runtime code from where you need to access the new settings value, + find the value in the ``registry.settings`` dictionary and use it. In :term:`view` code (or any other code that has access to the request), the easiest way to do this is via ``request.registry.settings``. For example: .. code-block:: python - registry = request.registry.settings + settings = request.registry.settings debug_frobnosticator = settings['debug_frobnosticator'] - If you wish to use the value in code that does not have access to the - request and you wish to use the value, you'll need to use the + If you wish to use the value in code that does not have access to the request + and you wish to use the value, you'll need to use the :func:`pyramid.threadlocal.get_current_registry` API to obtain the current registry, then ask for its ``settings`` attribute. For example: @@ -442,7 +553,3 @@ Here's how: registry = pyramid.threadlocal.get_current_registry() settings = registry.settings debug_frobnosticator = settings['debug_frobnosticator'] - - - - diff --git a/docs/narr/events.rst b/docs/narr/events.rst index 929208083..c10d4cc47 100644 --- a/docs/narr/events.rst +++ b/docs/narr/events.rst @@ -9,42 +9,39 @@ .. _events_chapter: Using Events -============= +============ -An *event* is an object broadcast by the :app:`Pyramid` framework -at interesting points during the lifetime of an application. You -don't need to use events in order to create most :app:`Pyramid` -applications, but they can be useful when you want to perform slightly -advanced operations. For example, subscribing to an event can allow -you to run some code as the result of every new request. +An *event* is an object broadcast by the :app:`Pyramid` framework at +interesting points during the lifetime of an application. You don't need to +use events in order to create most :app:`Pyramid` applications, but they can be +useful when you want to perform slightly advanced operations. For example, +subscribing to an event can allow you to run some code as the result of every +new request. -Events in :app:`Pyramid` are always broadcast by the framework. -However, they only become useful when you register a *subscriber*. A -subscriber is a function that accepts a single argument named `event`: +Events in :app:`Pyramid` are always broadcast by the framework. However, they +only become useful when you register a *subscriber*. A subscriber is a +function that accepts a single argument named `event`: .. code-block:: python :linenos: def mysubscriber(event): - print event + print(event) -The above is a subscriber that simply prints the event to the console -when it's called. +The above is a subscriber that simply prints the event to the console when it's +called. The mere existence of a subscriber function, however, is not sufficient to arrange for it to be called. To arrange for the subscriber to be called, -you'll need to use the -:meth:`pyramid.config.Configurator.add_subscriber` method or you'll -need to use the :func:`pyramid.events.subscriber` decorator to decorate a -function found via a :term:`scan`. +you'll need to use the :meth:`pyramid.config.Configurator.add_subscriber` +method or you'll need to use the :func:`pyramid.events.subscriber` decorator to +decorate a function found via a :term:`scan`. Configuring an Event Listener Imperatively ------------------------------------------ -You can imperatively configure a subscriber function to be called -for some event type via the -:meth:`~pyramid.config.Configurator.add_subscriber` -method (see also :term:`Configurator`): +You can imperatively configure a subscriber function to be called for some +event type via the :meth:`~pyramid.config.Configurator.add_subscriber` method: .. code-block:: python :linenos: @@ -53,21 +50,24 @@ method (see also :term:`Configurator`): from subscribers import mysubscriber - # "config" below is assumed to be an instance of a + # "config" below is assumed to be an instance of a # pyramid.config.Configurator object config.add_subscriber(mysubscriber, NewRequest) -The first argument to -:meth:`~pyramid.config.Configurator.add_subscriber` is the -subscriber function (or a :term:`dotted Python name` which refers -to a subscriber callable); the second argument is the event type. +The first argument to :meth:`~pyramid.config.Configurator.add_subscriber` is +the subscriber function (or a :term:`dotted Python name` which refers to a +subscriber callable); the second argument is the event type. + +.. seealso:: + + See also :term:`Configurator`. Configuring an Event Listener Using a Decorator ----------------------------------------------- -You can configure a subscriber function to be called for some event -type via the :func:`pyramid.events.subscriber` function. +You can configure a subscriber function to be called for some event type via +the :func:`pyramid.events.subscriber` function. .. code-block:: python :linenos: @@ -77,11 +77,11 @@ type via the :func:`pyramid.events.subscriber` function. @subscriber(NewRequest) def mysubscriber(event): - event.request.foo = 1 + event.request.foo = 1 -When the :func:`~pyramid.events.subscriber` decorator is used a -:term:`scan` must be performed against the package containing the -decorated function for the decorator to have any effect. +When the :func:`~pyramid.events.subscriber` decorator is used, a :term:`scan` +must be performed against the package containing the decorated function for the +decorator to have any effect. Either of the above registration examples implies that every time the :app:`Pyramid` framework emits an event object that supplies an @@ -92,13 +92,12 @@ As you can see, a subscription is made in terms of a *class* (such as :class:`pyramid.events.NewResponse`). The event object sent to a subscriber will always be an object that possesses an :term:`interface`. For :class:`pyramid.events.NewResponse`, that interface is -:class:`pyramid.interfaces.INewResponse`. The interface documentation -provides information about available attributes and methods of the event -objects. +:class:`pyramid.interfaces.INewResponse`. The interface documentation provides +information about available attributes and methods of the event objects. -The return value of a subscriber function is ignored. Subscribers to -the same event type are not guaranteed to be called in any particular -order relative to each other. +The return value of a subscriber function is ignored. Subscribers to the same +event type are not guaranteed to be called in any particular order relative to +each other. All the concrete :app:`Pyramid` event types are documented in the :ref:`events_module` API documentation. @@ -106,21 +105,20 @@ All the concrete :app:`Pyramid` event types are documented in the An Example ---------- -If you create event listener functions in a ``subscribers.py`` file in -your application like so: +If you create event listener functions in a ``subscribers.py`` file in your +application like so: .. code-block:: python :linenos: def handle_new_request(event): - print 'request', event.request + print('request', event.request) def handle_new_response(event): - print 'response', event.response + print('response', event.response) -You may configure these functions to be called at the appropriate -times by adding the following code to your application's -configuration startup: +You may configure these functions to be called at the appropriate times by +adding the following code to your application's configuration startup: .. code-block:: python :linenos: @@ -132,21 +130,100 @@ configuration startup: config.add_subscriber('myproject.subscribers.handle_new_response', 'pyramid.events.NewResponse') -Either mechanism causes the functions in ``subscribers.py`` to be -registered as event subscribers. Under this configuration, when the -application is run, each time a new request or response is detected, a -message will be printed to the console. +Either mechanism causes the functions in ``subscribers.py`` to be registered as +event subscribers. Under this configuration, when the application is run, each +time a new request or response is detected, a message will be printed to the +console. -Each of our subscriber functions accepts an ``event`` object and -prints an attribute of the event object. This begs the question: how -can we know which attributes a particular event has? +Each of our subscriber functions accepts an ``event`` object and prints an +attribute of the event object. This begs the question: how can we know which +attributes a particular event has? We know that :class:`pyramid.events.NewRequest` event objects have a -``request`` attribute, which is a :term:`request` object, because the -interface defined at :class:`pyramid.interfaces.INewRequest` says it must. -Likewise, we know that :class:`pyramid.interfaces.NewResponse` events have a -``response`` attribute, which is a response object constructed by your -application, because the interface defined at -:class:`pyramid.interfaces.INewResponse` says it must +``request`` attribute, which is a :term:`request` object, because the interface +defined at :class:`pyramid.interfaces.INewRequest` says it must. Likewise, we +know that :class:`pyramid.interfaces.NewResponse` events have a ``response`` +attribute, which is a response object constructed by your application, because +the interface defined at :class:`pyramid.interfaces.INewResponse` says it must (:class:`pyramid.events.NewResponse` objects also have a ``request``). +.. _custom_events: + +Creating Your Own Events +------------------------ + +In addition to using the events that the Pyramid framework creates, you can +create your own events for use in your application. This can be useful to +decouple parts of your application. + +For example, suppose your application has to do many things when a new document +is created. Rather than putting all this logic in the view that creates the +document, you can create the document in your view and then fire a custom +event. Subscribers to the custom event can take other actions, such as indexing +the document, sending email, or sending a message to a remote system. + +An event is simply an object. There are no required attributes or method for +your custom events. In general, your events should keep track of the +information that subscribers will need. Here are some example custom event +classes: + +.. code-block:: python + :linenos: + + class DocCreated(object): + def __init__(self, doc, request): + self.doc = doc + self.request = request + + class UserEvent(object): + def __init__(self, user): + self.user = user + + class UserLoggedIn(UserEvent): + pass + +Some Pyramid applications choose to define custom events classes in an +``events`` module. + +You can subscribe to custom events in the same way that you subscribe to +Pyramid events—either imperatively or with a decorator. You can also use custom +events with :ref:`subscriber predicates <subscriber_predicates>`. Here's an +example of subscribing to a custom event with a decorator: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from .events import DocCreated + from .index import index_doc + + @subscriber(DocCreated) + def index_doc(event): + # index the document using our application's index_doc function + index_doc(event.doc, event.request) + +The above example assumes that the application defines a ``DocCreated`` event +class and an ``index_doc`` function. + +To fire your custom events use the :meth:`pyramid.registry.Registry.notify` +method, which is most often accessed as ``request.registry.notify``. For +example: + +.. code-block:: python + :linenos: + + from .events import DocCreated + + def new_doc_view(request): + doc = MyDoc() + event = DocCreated(doc, request) + request.registry.notify(event) + return {'document': doc} + +This example view will notify all subscribers to the custom ``DocCreated`` +event. + +Note that when you fire an event, all subscribers are run synchronously so it's +generally not a good idea to create event handlers that may take a long time to +run. Although event handlers could be used as a central place to spawn tasks on +your own message queues. diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst new file mode 100644 index 000000000..af7d0a349 --- /dev/null +++ b/docs/narr/extconfig.rst @@ -0,0 +1,462 @@ +.. index:: + single: extending configuration + +.. _extconfig_narr: + +Extending Pyramid Configuration +=============================== + +Pyramid allows you to extend its Configurator with custom directives. Custom +directives can use other directives, they can add a custom :term:`action`, they +can participate in :term:`conflict resolution`, and they can provide some +number of :term:`introspectable` objects. + +.. index:: + single: add_directive + pair: configurator; adding directives + +.. _add_directive: + +Adding Methods to the Configurator via ``add_directive`` +-------------------------------------------------------- + +Framework extension writers can add arbitrary methods to a :term:`Configurator` +by using the :meth:`pyramid.config.Configurator.add_directive` method of the +configurator. Using :meth:`~pyramid.config.Configurator.add_directive` makes it +possible to extend a Pyramid configurator in arbitrary ways, and allows it to +perform application-specific tasks more succinctly. + +The :meth:`~pyramid.config.Configurator.add_directive` method accepts two +positional arguments: a method name and a callable object. The callable object +is usually a function that takes the configurator instance as its first +argument and accepts other arbitrary positional and keyword arguments. For +example: + +.. code-block:: python + :linenos: + + from pyramid.events import NewRequest + from pyramid.config import Configurator + + def add_newrequest_subscriber(config, subscriber): + config.add_subscriber(subscriber, NewRequest) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can +then call the added directive by its given name as if it were a built-in method +of the Configurator: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print(event.request) + + config.add_newrequest_subscriber(mysubscriber) + +A call to :meth:`~pyramid.config.Configurator.add_directive` is often "hidden" +within an ``includeme`` function within a "frameworky" package meant to be +included as per :ref:`including_configuration` via +:meth:`~pyramid.config.Configurator.include`. For example, if you put this +code in a package named ``pyramid_subscriberhelpers``: + +.. code-block:: python + :linenos: + + def includeme(config): + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +The user of the add-on package ``pyramid_subscriberhelpers`` would then be able +to install it and subsequently do: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print(event.request) + + from pyramid.config import Configurator + config = Configurator() + config.include('pyramid_subscriberhelpers') + config.add_newrequest_subscriber(mysubscriber) + +Using ``config.action`` in a Directive +-------------------------------------- + +If a custom directive can't do its work exclusively in terms of existing +configurator methods (such as +:meth:`pyramid.config.Configurator.add_subscriber` as above), the directive may +need to make use of the :meth:`pyramid.config.Configurator.action` method. This +method adds an entry to the list of "actions" that Pyramid will attempt to +process when :meth:`pyramid.config.Configurator.commit` is called. An action is +simply a dictionary that includes a :term:`discriminator`, possibly a callback +function, and possibly other metadata used by Pyramid's action system. + +Here's an example directive which uses the "action" method: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(): + config.registry.jammyjam = jammyjam + config.action('jammyjam', register) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +Fancy, but what does it do? The action method accepts a number of arguments. +In the above directive named ``add_jammyjam``, we call +:meth:`~pyramid.config.Configurator.action` with two arguments: the string +``jammyjam`` is passed as the first argument named ``discriminator``, and the +closure function named ``register`` is passed as the second argument named +``callable``. + +When the :meth:`~pyramid.config.Configurator.action` method is called, it +appends an action to the list of pending configuration actions. All pending +actions with the same discriminator value are potentially in conflict with one +another (see :ref:`conflict_detection`). When the +:meth:`~pyramid.config.Configurator.commit` method of the Configurator is +called (either explicitly or as the result of calling +:meth:`~pyramid.config.Configurator.make_wsgi_app`), conflicting actions are +potentially automatically resolved as per :ref:`automatic_conflict_resolution`. +If a conflict cannot be automatically resolved, a +:exc:`pyramid.exceptions.ConfigurationConflictError` is raised and application +startup is prevented. + +In our above example, therefore, if a consumer of our ``add_jammyjam`` +directive did this: + +.. code-block:: python + + config.add_jammyjam('first') + config.add_jammyjam('second') + +When the action list was committed resulting from the set of calls above, our +user's application would not start, because the discriminators of the actions +generated by the two calls are in direct conflict. Automatic conflict +resolution cannot resolve the conflict (because no ``config.include`` is +involved), and the user provided no intermediate +:meth:`pyramid.config.Configurator.commit` call between the calls to +``add_jammyjam`` to ensure that the successive calls did not conflict with each +other. + +This demonstrates the purpose of the discriminator argument to the action +method: it's used to indicate a uniqueness constraint for an action. Two +actions with the same discriminator will conflict unless the conflict is +automatically or manually resolved. A discriminator can be any hashable object, +but it is generally a string or a tuple. *You use a discriminator to +declaratively ensure that the user doesn't provide ambiguous configuration +statements.* + +But let's imagine that a consumer of ``add_jammyjam`` used it in such a way +that no configuration conflicts are generated. + +.. code-block:: python + + config.add_jammyjam('first') + +What happens now? When the ``add_jammyjam`` method is called, an action is +appended to the pending actions list. When the pending configuration actions +are processed during :meth:`~pyramid.config.Configurator.commit`, and no +conflicts occur, the *callable* provided as the second argument to the +:meth:`~pyramid.config.Configurator.action` method within ``add_jammyjam`` is +called with no arguments. The callable in ``add_jammyjam`` is the ``register`` +closure function. It simply sets the value ``config.registry.jammyjam`` to +whatever the user passed in as the ``jammyjam`` argument to the +``add_jammyjam`` function. Therefore, the result of the user's call to our +directive will set the ``jammyjam`` attribute of the registry to the string +``first``. *A callable is used by a directive to defer the result of a user's +call to the directive until conflict detection has had a chance to do its job*. + +Other arguments exist to the :meth:`~pyramid.config.Configurator.action` +method, including ``args``, ``kw``, ``order``, and ``introspectables``. + +``args`` and ``kw`` exist as values, which if passed will be used as arguments +to the ``callable`` function when it is called back. For example, our +directive might use them like so: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(*arg, **kw): + config.registry.jammyjam_args = arg + config.registry.jammyjam_kw = kw + config.registry.jammyjam = jammyjam + config.action('jammyjam', register, args=('one',), kw={'two':'two'}) + +In the above example, when this directive is used to generate an action, and +that action is committed, ``config.registry.jammyjam_args`` will be set to +``('one',)`` and ``config.registry.jammyjam_kw`` will be set to +``{'two':'two'}``. ``args`` and ``kw`` are honestly not very useful when your +``callable`` is a closure function, because you already usually have access to +every local in the directive without needing them to be passed back. They can +be useful, however, if you don't use a closure as a callable. + +``order`` is a crude order control mechanism. ``order`` defaults to the +integer ``0``; it can be set to any other integer. All actions that share an +order will be called before other actions that share a higher order. This +makes it possible to write a directive with callable logic that relies on the +execution of the callable of another directive being done first. For example, +Pyramid's :meth:`pyramid.config.Configurator.add_view` directive registers an +action with a higher order than the +:meth:`pyramid.config.Configurator.add_route` method. Due to this, the +``add_view`` method's callable can assume that, if a ``route_name`` was passed +to it, that a route by this name was already registered by ``add_route``, and +if such a route has not already been registered, it's a configuration error (a +view that names a nonexistent route via its ``route_name`` parameter will never +be called). + +.. versionchanged:: 1.6 + As of Pyramid 1.6 it is possible for one action to invoke another. See + :ref:`ordering_actions` for more information. + +Finally, ``introspectables`` is a sequence of :term:`introspectable` objects. +You can pass a sequence of introspectables to the +:meth:`~pyramid.config.Configurator.action` method, which allows you to augment +Pyramid's configuration introspection system. + +.. _ordering_actions: + +Ordering Actions +---------------- + +In Pyramid every :term:`action` has an inherent ordering relative to other +actions. The logic within actions is deferred until a call to +:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by +:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call +``config.add_view(route_name='foo')`` **before** ``config.add_route('foo', +'/foo')`` because nothing actually happens until commit-time. During a commit +cycle, conflicts are resolved, and actions are ordered and executed. + +By default, almost every action in Pyramid has an ``order`` of +:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level +will be executed in the order it was called. This means that if an action must +be reliably executed before or after another action, the ``order`` must be +defined explicitly to make this work. For example, views are dependent on +routes being defined. Thus the action created by +:meth:`pyramid.config.Configurator.add_route` has an ``order`` of +:const:`pyramid.config.PHASE2_CONFIG`. + +Pre-defined Phases +~~~~~~~~~~~~~~~~~~ + +:const:`pyramid.config.PHASE0_CONFIG` + +- This phase is reserved for developers who want to execute actions prior to + Pyramid's core directives. + +:const:`pyramid.config.PHASE1_CONFIG` + +- :meth:`pyramid.config.Configurator.add_renderer` +- :meth:`pyramid.config.Configurator.add_route_predicate` +- :meth:`pyramid.config.Configurator.add_subscriber_predicate` +- :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.add_view_deriver` +- :meth:`pyramid.config.Configurator.set_authorization_policy` +- :meth:`pyramid.config.Configurator.set_default_permission` +- :meth:`pyramid.config.Configurator.set_view_mapper` + +:const:`pyramid.config.PHASE2_CONFIG` + +- :meth:`pyramid.config.Configurator.add_route` +- :meth:`pyramid.config.Configurator.set_authentication_policy` + +:const:`pyramid.config.PHASE3_CONFIG` + +- The default for all builtin or custom directives unless otherwise specified. + +Calling Actions from Actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Pyramid's configurator allows actions to be added during a commit-cycle as long +as they are added to the current or a later ``order`` phase. This means that +your custom action can defer decisions until commit-time and then do things +like invoke :meth:`pyramid.config.Configurator.add_route`. It can also provide +better conflict detection if your addon needs to call more than one other +action. + +For example, let's make an addon that invokes ``add_route`` and ``add_view``, +but we want it to conflict with any other call to our addon: + +.. code-block:: python + :linenos: + + from pyramid.config import PHASE0_CONFIG + + def includeme(config): + config.add_directive('add_auto_route', add_auto_route) + + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action(('auto route', name), register, order=PHASE0_CONFIG) + +Now someone else can use your addon and be informed if there is a conflict +between this route and another, or two calls to ``add_auto_route``. Notice how +we had to invoke our action **before** ``add_view`` or ``add_route``. If we +tried to invoke this afterward, the subsequent calls to ``add_view`` and +``add_route`` would cause conflicts because that phase had already been +executed, and the configurator cannot go back in time to add more views during +that commit-cycle. + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('auto_route_addon') + config.add_auto_route('foo', my_view) + + def my_view(request): + return request.response + +.. _introspection: + +Adding Configuration Introspection +---------------------------------- + +.. versionadded:: 1.3 + +Pyramid provides a configuration introspection system that can be used by +debugging tools to provide visibility into the configuration of a running +application. + +All built-in Pyramid directives (such as +:meth:`pyramid.config.Configurator.add_view` and +:meth:`pyramid.config.Configurator.add_route`) register a set of +introspectables when called. For example, when you register a view via +``add_view``, the directive registers at least one introspectable: an +introspectable about the view registration itself, providing human-consumable +values for the arguments passed into it. You can later use the introspection +query system to determine whether a particular view uses a renderer, or whether +a particular view is limited to a particular request method, or against which +routes a particular view is registered. The Pyramid "debug toolbar" makes use +of the introspection system in various ways to display information to Pyramid +developers. + +Introspection values are set when a sequence of :term:`introspectable` objects +is passed to the :meth:`~pyramid.config.Configurator.action` method. Here's an +example of a directive which uses introspectables: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value): + def register(): + config.registry.jammyjam = value + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + config.action('jammyjam', register, introspectables=(intr,)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +If you notice, the above directive uses the ``introspectable`` attribute of a +Configurator (:attr:`pyramid.config.Configurator.introspectable`) to create an +introspectable object. The introspectable object's constructor requires at +least four arguments: the ``category_name``, the ``discriminator``, the +``title``, and the ``type_name``. + +The ``category_name`` is a string representing the logical category for this +introspectable. Usually the category_name is a pluralization of the type of +object being added via the action. + +The ``discriminator`` is a value unique **within the category** (unlike the +action discriminator, which must be unique within the entire set of actions). +It is typically a string or tuple representing the values unique to this +introspectable within the category. It is used to generate links and as part +of a relationship-forming target for other introspectables. + +The ``title`` is a human-consumable string that can be used by introspection +system frontends to show a friendly summary of this introspectable. + +The ``type_name`` is a value that can be used to subtype this introspectable +within its category for sorting and presentation purposes. It can be any +value. + +An introspectable is also dictionary-like. It can contain any set of key/value +pairs, typically related to the arguments passed to its related directive. +While the ``category_name``, ``discriminator``, ``title``, and ``type_name`` +are *metadata* about the introspectable, the values provided as key/value pairs +are the actual data provided by the introspectable. In the above example, we +set the ``value`` key to the value of the ``value`` argument passed to the +directive. + +Our directive above mutates the introspectable, and passes it in to the +``action`` method as the first element of a tuple as the value of the +``introspectable`` keyword argument. This associates this introspectable with +the action. Introspection tools will then display this introspectable in their +index. + +Introspectable Relationships +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two introspectables may have relationships between each other. + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value, template): + def register(): + config.registry.jammyjam = (value, template) + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + tmpl_intr = config.introspectable(category_name='jammyjam templates', + discriminator=template, + title=template, + type_name=None) + tmpl_intr['value'] = template + intr.relate('jammyjam templates', template) + config.action('jammyjam', register, introspectables=(intr, tmpl_intr)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +In the above example, the ``add_jammyjam`` directive registers two +introspectables: the first is related to the ``value`` passed to the directive, +and the second is related to the ``template`` passed to the directive. If you +believe a concept within a directive is important enough to have its own +introspectable, you can cause the same directive to register more than one +introspectable, registering one introspectable for the "main idea" and another +for a related concept. + +The call to ``intr.relate`` above +(:meth:`pyramid.interfaces.IIntrospectable.relate`) is passed two arguments: a +category name and a directive. The example above effectively indicates that +the directive wishes to form a relationship between the ``intr`` introspectable +and the ``tmpl_intr`` introspectable; the arguments passed to ``relate`` are +the category name and discriminator of the ``tmpl_intr`` introspectable. + +Relationships need not be made between two introspectables created by the same +directive. Instead a relationship can be formed between an introspectable +created in one directive and another introspectable created in another by +calling ``relate`` on either side with the other directive's category name and +discriminator. An error will be raised at configuration commit time if you +attempt to relate an introspectable with another nonexistent introspectable, +however. + +Introspectable relationships will show up in frontend system renderings of +introspection values. For example, if a view registration names a route name, +the introspectable related to the view callable will show a reference to the +route to which it relates and vice versa. diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index f62c7e6bb..9dc042024 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -1,13 +1,13 @@ .. _extending_chapter: -Extending An Existing :app:`Pyramid` Application -=================================================== +Extending an Existing :app:`Pyramid` Application +================================================ -If a :app:`Pyramid` developer has obeyed certain constraints while building -an application, a third party should be able to change the application's -behavior without needing to modify its source code. The behavior of a -:app:`Pyramid` application that obeys certain constraints can be *overridden* -or *extended* without modification. +If a :app:`Pyramid` developer has obeyed certain constraints while building an +application, a third party should be able to change the application's behavior +without needing to modify its source code. The behavior of a :app:`Pyramid` +application that obeys certain constraints can be *overridden* or *extended* +without modification. We'll define some jargon here for the benefit of identifying the parties involved in such an effort. @@ -16,10 +16,10 @@ Developer The original application developer. Integrator - Another developer who wishes to reuse the application written by the - original application developer in an unanticipated context. He may also - wish to modify the original application without changing the original - application's source code. + Another developer who wishes to reuse the application written by the original + application developer in an unanticipated context. They may also wish to + modify the original application without changing the original application's + source code. The Difference Between "Extensible" and "Pluggable" Applications ---------------------------------------------------------------- @@ -27,31 +27,31 @@ The Difference Between "Extensible" and "Pluggable" Applications Other web frameworks, such as :term:`Django`, advertise that they allow developers to create "pluggable applications". They claim that if you create an application in a certain way, it will be integratable in a sensible, -structured way into another arbitrarily-written application or project -created by a third-party developer. +structured way into another arbitrarily-written application or project created +by a third-party developer. :app:`Pyramid`, as a platform, does not claim to provide such a feature. The platform provides no guarantee that you can create an application and package it up such that an arbitrary integrator can use it as a subcomponent in a larger Pyramid application or project. Pyramid does not mandate the constraints necessary for such a pattern to work satisfactorily. Because -Pyramid is not very "opinionated", developers are able to use wildly -different patterns and technologies to build an application. A given Pyramid -application may happen to be reusable by a particular third party integrator, -because the integrator and the original developer may share similar base -technology choices (such as the use of a particular relational database or -ORM). But the same application may not be reusable by a different developer, -because he has made different technology choices which are incompatible with -the original developer's. +Pyramid is not very "opinionated", developers are able to use wildly different +patterns and technologies to build an application. A given Pyramid application +may happen to be reusable by a particular third party integrator because the +integrator and the original developer may share similar base technology choices +(such as the use of a particular relational database or ORM). But the same +application may not be reusable by a different developer, because they have +made different technology choices which are incompatible with the original +developer's. As a result, the concept of a "pluggable application" is left to layers built above Pyramid, such as a "CMS" layer or "application server" layer. Such -layers are apt to provide the necessary "opinions" (such as mandating a -storage layer, a templating system, and a structured, well-documented pattern -of registering that certain URLs map to certain bits of code) which makes the -concept of a "pluggable application" possible. "Pluggable applications", -thus, should not plug in to Pyramid itself but should instead plug into a -system written atop Pyramid. +layers are apt to provide the necessary "opinions" (such as mandating a storage +layer, a templating system, and a structured, well-documented pattern of +registering that certain URLs map to certain bits of code) which makes the +concept of a "pluggable application" possible. "Pluggable applications", thus, +should not plug into Pyramid itself but should instead plug into a system +written atop Pyramid. Although it does not provide for "pluggable applications", Pyramid *does* provide a rich set of mechanisms which allows for the extension of a single @@ -62,13 +62,15 @@ Pyramid applications are *extensible*. .. index:: single: extensible application -Rules for Building An Extensible Application +.. _building_an_extensible_app: + +Rules for Building an Extensible Application -------------------------------------------- There is only one rule you need to obey if you want to build a maximally extensible :app:`Pyramid` application: as a developer, you should factor any -overrideable :term:`imperative configuration` you've created into functions -which can be used via :meth:`pyramid.config.Configurator.include` rather than +overridable :term:`imperative configuration` you've created into functions +which can be used via :meth:`pyramid.config.Configurator.include`, rather than inlined as calls to methods of a :term:`Configurator` within the ``main`` function in your application's ``__init__.py``. For example, rather than: @@ -82,8 +84,8 @@ function in your application's ``__init__.py``. For example, rather than: config.add_view('myapp.views.view1', name='view1') config.add_view('myapp.views.view2', name='view2') -You should do move the calls to ``add_view`` outside of the (non-reusable) -``if __name__ == '__main__'`` block, and into a reusable function: +You should move the calls to ``add_view`` outside of the (non-reusable) ``if +__name__ == '__main__'`` block, and into a reusable function: .. code-block:: python :linenos: @@ -98,13 +100,12 @@ You should do move the calls to ``add_view`` outside of the (non-reusable) config.add_view('myapp.views.view1', name='view1') config.add_view('myapp.views.view2', name='view2') -Doing this allows an integrator to maximally reuse the configuration -statements that relate to your application by allowing him to selectively -include or disinclude the configuration functions you've created from an -"override package". +Doing this allows an integrator to maximally reuse the configuration statements +that relate to your application by allowing them to selectively include or +exclude the configuration functions you've created from an "override package". -Alternately, you can use :term:`ZCML` for the purpose of making configuration -extensible and overrideable. :term:`ZCML` declarations that belong to an +Alternatively you can use :term:`ZCML` for the purpose of making configuration +extensible and overridable. :term:`ZCML` declarations that belong to an application can be overridden and extended by integrators as necessary in a similar fashion. If you use only :term:`ZCML` to configure your application, it will automatically be maximally extensible without any manual effort. See @@ -113,16 +114,15 @@ it will automatically be maximally extensible without any manual effort. See Fundamental Plugpoints ~~~~~~~~~~~~~~~~~~~~~~ -The fundamental "plug points" of an application developed using -:app:`Pyramid` are *routes*, *views*, and *assets*. Routes are declarations -made using the :meth:`pyramid.config.Configurator.add_route` method. Views -are declarations made using the :meth:`pyramid.config.Configurator.add_view` -method. Assets are files that are -accessed by :app:`Pyramid` using the :term:`pkg_resources` API such as static -files and templates via a :term:`asset specification`. Other directives and -configurator methods also deal in routes, views, and assets. For example, the -``add_handler`` directive of the ``pyramid_handlers`` package adds a single -route, and some number of views. +The fundamental "plug points" of an application developed using :app:`Pyramid` +are *routes*, *views*, and *assets*. Routes are declarations made using the +:meth:`pyramid.config.Configurator.add_route` method. Views are declarations +made using the :meth:`pyramid.config.Configurator.add_view` method. Assets are +files that are accessed by :app:`Pyramid` using the :term:`pkg_resources` API +such as static files and templates via a :term:`asset specification`. Other +directives and configurator methods also deal in routes, views, and assets. +For example, the ``add_handler`` directive of the ``pyramid_handlers`` package +adds a single route and some number of views. .. index:: single: extending an existing application @@ -131,10 +131,9 @@ Extending an Existing Application --------------------------------- The steps for extending an existing application depend largely on whether the -application does or does not use configuration decorators and/or imperative -code. +application does or does not use configuration decorators or imperative code. -If The Application Has Configuration Decorations +If the Application Has Configuration Decorations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You've inherited a :app:`Pyramid` application which you'd like to extend or @@ -153,9 +152,9 @@ registers more views or routes. config.add_view('mypackage.views.myview', name='myview') If you want to *override* configuration in the application, you *may* need to -run :meth:`pyramid.config.Configurator.commit` after performing the scan of -the original package, then add additional configuration that registers more -views or routes which performs overrides. +run :meth:`pyramid.config.Configurator.commit` after performing the scan of the +original package, then add additional configuration that registers more views +or routes which perform overrides. .. code-block:: python :linenos: @@ -168,11 +167,11 @@ views or routes which performs overrides. Once this is done, you should be able to extend or override the application like any other (see :ref:`extending_the_application`). -You can alternately just prevent a :term:`scan` from happening (by omitting -any call to the :meth:`pyramid.config.Configurator.scan` method). This will +You can alternatively just prevent a :term:`scan` from happening by omitting +any call to the :meth:`pyramid.config.Configurator.scan` method. This will cause the decorators attached to objects in the target application to do -nothing. At this point, you will need to convert all the configuration done -in decorators into equivalent imperative configuration or ZCML and add that +nothing. At this point, you will need to convert all the configuration done in +decorators into equivalent imperative configuration or ZCML, and add that configuration or ZCML to a separate Python package as described in :ref:`extending_the_application`. @@ -181,37 +180,37 @@ configuration or ZCML to a separate Python package as described in Extending the Application ~~~~~~~~~~~~~~~~~~~~~~~~~ -To extend or override the behavior of an existing application, you will need -to create a new package which includes the configuration of the old package, -and you'll perhaps need to create implementations of the types of things -you'd like to override (such as views), which are referred to within the -original package. +To extend or override the behavior of an existing application, you will need to +create a new package which includes the configuration of the old package, and +you'll perhaps need to create implementations of the types of things you'd like +to override (such as views), to which they are referred within the original +package. -The general pattern for extending an existing application looks something -like this: +The general pattern for extending an existing application looks something like +this: - Create a new Python package. The easiest way to do this is to create a new :app:`Pyramid` application using the scaffold mechanism. See :ref:`creating_a_project` for more information. -- In the new package, create Python files containing views and other - overridden elements, such as templates and static assets as necessary. +- In the new package, create Python files containing views and other overridden + elements, such as templates and static assets as necessary. - Install the new package into the same Python environment as the original - application (e.g. ``python setup.py develop`` or ``python setup.py - install``). + application (e.g., ``$VENV/bin/pip install -e .`` or ``$VENV/bin/pip install + .``). - Change the ``main`` function in the new package's ``__init__.py`` to include the original :app:`Pyramid` application's configuration functions via :meth:`pyramid.config.Configurator.include` statements or a :term:`scan`. -- Wire the new views and assets created in the new package up using - imperative registrations within the ``main`` function of the - ``__init__.py`` file of the new application. These wiring should happen - *after* including the configuration functions of the old application. - These registrations will extend or override any registrations performed by - the original application. See :ref:`overriding_views`, - :ref:`overriding_routes` and :ref:`overriding_resources`. +- Wire the new views and assets created in the new package up using imperative + registrations within the ``main`` function of the ``__init__.py`` file of the + new application. This wiring should happen *after* including the + configuration functions of the old application. These registrations will + extend or override any registrations performed by the original application. + See :ref:`overriding_views`, :ref:`overriding_routes`, and + :ref:`overriding_resources`. .. index:: pair: overriding; views @@ -219,20 +218,20 @@ like this: .. _overriding_views: Overriding Views -~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ -The :term:`view configuration` declarations you make which *override* +The :term:`view configuration` declarations that you make which *override* application behavior will usually have the same :term:`view predicate` -attributes as the original you wish to override. These ``<view>`` -declarations will point at "new" view code, in the override package you've -created. The new view code itself will usually be cut-n-paste copies of view -callables from the original application with slight tweaks. +attributes as the original that you wish to override. These ``<view>`` +declarations will point at "new" view code in the override package that you've +created. The new view code itself will usually be copy-and-paste copies of +view callables from the original application with slight tweaks. -For example, if the original application has the following -``configure_views`` configuration method: +For example, if the original application has the following ``configure_views`` +configuration method: .. code-block:: python - :linenos: + :linenos: def configure_views(config): config.add_view('theoriginalapp.views.theview', name='theview') @@ -252,13 +251,13 @@ configuration function: config.include(configure_views) config.add_view('theoverrideapp.views.theview', name='theview') -In this case, the ``theoriginalapp.views.theview`` view will never be -executed. Instead, a new view, ``theoverrideapp.views.theview`` will be -executed instead, when request circumstances dictate. +In this case, the ``theoriginalapp.views.theview`` view will never be executed. +Instead, a new view, ``theoverrideapp.views.theview`` will be executed when +request circumstances dictate. A similar pattern can be used to *extend* the application with ``add_view`` -declarations. Just register a new view against some other set of predicates -to make sure the URLs it implies are available on some other page rendering. +declarations. Just register a new view against some other set of predicates to +make sure the URLs it implies are available on some other page rendering. .. index:: pair: overriding; routes @@ -268,13 +267,13 @@ to make sure the URLs it implies are available on some other page rendering. Overriding Routes ~~~~~~~~~~~~~~~~~ -Route setup is currently typically performed in a sequence of ordered calls -to :meth:`~pyramid.config.Configurator.add_route`. Because these calls are +Route setup is currently typically performed in a sequence of ordered calls to +:meth:`~pyramid.config.Configurator.add_route`. Because these calls are ordered relative to each other, and because this ordering is typically important, you should retain their relative ordering when performing an -override. Typically, this means *copying* all the ``add_route`` statements -into the override package's file and changing them as necessary. Then -disinclude any ``add_route`` statements from the original application. +override. Typically this means *copying* all the ``add_route`` statements into +the override package's file and changing them as necessary. Then exclude any +``add_route`` statements from the original application. .. index:: pair: overriding; assets @@ -286,9 +285,8 @@ Overriding Assets Assets are files on the filesystem that are accessible within a Python *package*. An entire chapter is devoted to assets: :ref:`assets_chapter`. -Within this chapter is a section named :ref:`overriding_assets_section`. -This section of that chapter describes in detail how to override package -assets with other assets by using the -:meth:`pyramid.config.Configurator.override_asset` method. Add such -``override_asset`` calls to your override package's ``__init__.py`` to -perform overrides. +Within this chapter is a section named :ref:`overriding_assets_section`. This +section of that chapter describes in detail how to override package assets with +other assets by using the :meth:`pyramid.config.Configurator.override_asset` +method. Add such ``override_asset`` calls to your override package's +``__init__.py`` to perform overrides. diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index f5adad905..6a952dec9 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -1,111 +1,101 @@ +.. index:: + single: hello world program + .. _firstapp_chapter: Creating Your First :app:`Pyramid` Application -================================================= +============================================== In this chapter, we will walk through the creation of a tiny :app:`Pyramid` application. After we're finished creating the application, we'll explain in -more detail how it works. +more detail how it works. It assumes you already have :app:`Pyramid` installed. +If you do not, head over to the :ref:`installing_chapter` section. .. _helloworld_imperative: -Hello World, Goodbye World --------------------------- +Hello World +----------- -Here's one of the very simplest :app:`Pyramid` applications, configured -imperatively: +Here's one of the very simplest :app:`Pyramid` applications: -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: - from pyramid.config import Configurator - from pyramid.response import Response - from paste.httpserver import serve +When this code is inserted into a Python script named ``helloworld.py`` and +executed by a Python interpreter which has the :app:`Pyramid` software +installed, an HTTP server is started on TCP port 8080. - def hello_world(request): - return Response('Hello world!') +On UNIX: - def goodbye_world(request): - return Response('Goodbye world!') +.. code-block:: text - if __name__ == '__main__': - config = Configurator() - config.add_view(hello_world) - config.add_view(goodbye_world, name='goodbye') - app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + $ $VENV/bin/python helloworld.py -When this code is inserted into a Python script named ``helloworld.py`` and -executed by a Python interpreter which has the :app:`Pyramid` software -installed, an HTTP server is started on TCP port 8080: +On Windows: .. code-block:: text - $ python helloworld.py - serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 + C:\> %VENV%\Scripts\python.exe helloworld.py + +This command will not return and nothing will be printed to the console. When +port 8080 is visited by a browser on the URL ``/hello/world``, the server will +simply serve up the text "Hello world!". If your application is running on +your local system, using `<http://localhost:8080/hello/world>`_ in a browser +will show this result. -When port 8080 is visited by a browser on the root URL (``/``), the server -will simply serve up the text "Hello world!" When visited by a browser on -the URL ``/goodbye``, the server will serve up the text "Goodbye world!" +Each time you visit a URL served by the application in a browser, a logging +line will be emitted to the console displaying the hostname, the date, the +request method and path, and some additional information. This output is done +by the wsgiref server we've used to serve this application. It logs an "access +log" in Apache combined logging format to the console. -Press ``Ctrl-C`` to stop the application. +Press ``Ctrl-C`` (or ``Ctrl-Break`` on Windows) to stop the application. Now that we have a rudimentary understanding of what the application does, -let's examine it piece-by-piece. +let's examine it piece by piece. Imports ~~~~~~~ -The above ``helloworld.py`` script uses the following set of import -statements: +The above ``helloworld.py`` script uses the following set of import statements: -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: - - from pyramid.config import Configurator - from pyramid.response import Response - from paste.httpserver import serve + :lines: 1-3 The script imports the :class:`~pyramid.config.Configurator` class from the :mod:`pyramid.config` module. An instance of the :class:`~pyramid.config.Configurator` class is later used to configure your :app:`Pyramid` application. -The script uses the :class:`pyramid.response.Response` class later in the -script to create a :term:`response` object. - Like many other Python web frameworks, :app:`Pyramid` uses the :term:`WSGI` protocol to connect an application and a web server together. The -:mod:`paste.httpserver` server is used in this example as a WSGI server for -convenience, as the ``paste`` package is a dependency of :app:`Pyramid` -itself. +:mod:`wsgiref` server is used in this example as a WSGI server for convenience, +as it is shipped within the Python standard library. + +The script also imports the :class:`pyramid.response.Response` class for later +use. An instance of this class will be used to create a web response. View Callable Declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~ -The above script, beneath its set of imports, defines two functions: one -named ``hello_world`` and one named ``goodbye_world``. +The above script, beneath its set of imports, defines a function named +``hello_world``. -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: + :pyobject: hello_world - def hello_world(request): - return Response('Hello world!') +The function accepts a single argument (``request``) and it returns an instance +of the :class:`pyramid.response.Response` class. The single argument to the +class' constructor is a string computed from parameters matched from the URL. +This value becomes the body of the response. - def goodbye_world(request): - return Response('Goodbye world!') - -These functions don't do anything very difficult. Both functions accept a -single argument (``request``). The ``hello_world`` function does nothing but -return a response instance with the body ``Hello world!``. The -``goodbye_world`` function returns a response instance with the body -``Goodbye world!``. - -Each of these functions is known as a :term:`view callable`. A view callable -accepts a single argument, ``request``. It is expected to return a -:term:`response` object. A view callable doesn't need to be a function; it -can be represented via another type of object, like a class or an instance, -but for our purposes here, a function serves us well. +This function is known as a :term:`view callable`. A view callable accepts a +single argument, ``request``. It is expected to return a :term:`response` +object. A view callable doesn't need to be a function; it can be represented +via another type of object, like a class or an instance, but for our purposes +here, a function serves us well. A view callable is always called with a :term:`request` object. A request object is a representation of an HTTP request sent to :app:`Pyramid` via the @@ -113,18 +103,11 @@ active :term:`WSGI` server. A view callable is required to return a :term:`response` object because a response object has all the information necessary to formulate an actual HTTP -response; this object is then converted to text by the upstream :term:`WSGI` -server and sent back to the requesting browser. To return a response, each -view callable creates an instance of the :class:`~pyramid.response.Response` -class. In the ``hello_world`` function, the string ``'Hello world!'`` is -passed to the ``Response`` constructor as the *body* of the response. In the -``goodbye_world`` function, the string ``'Goodbye world!'`` is passed. - -.. note:: As we'll see in later chapters, returning a literal - :term:`response` object from a view callable is not always required; we - can instead use a :term:`renderer` in our view configurations. If we use - a renderer, our view callable is allowed to return a value that the - renderer understands, and the renderer generates a response on our behalf. +response; this object is then converted to text by the :term:`WSGI` server +which called Pyramid and it is sent back to the requesting browser. To return +a response, each view callable creates an instance of the +:class:`~pyramid.response.Response` class. In the ``hello_world`` function, a +string is passed as the body to the response. .. index:: single: imperative configuration @@ -136,114 +119,63 @@ passed to the ``Response`` constructor as the *body* of the response. In the Application Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~ -In the above script, the following code represents the *configuration* of -this simple application. The application is configured using the previously -defined imports and function definitions, placed within the confines of an -``if`` statement: +In the above script, the following code represents the *configuration* of this +simple application. The application is configured using the previously defined +imports and function definitions, placed within the confines of an ``if`` +statement: -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: + :lines: 9-15 - if __name__ == '__main__': - config = Configurator() - config.add_view(hello_world) - config.add_view(goodbye_world, name='goodbye') - app = config.make_wsgi_app() - serve(app, host='0.0.0.0') - -Let's break this down this piece-by-piece. +Let's break this down piece by piece. Configurator Construction ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: - - if __name__ == '__main__': - config = Configurator() + :lines: 9-10 The ``if __name__ == '__main__':`` line in the code sample above represents a Python idiom: the code inside this if clause is not invoked unless the script -containing this code is run directly from the command line. For example, if -the file named ``helloworld.py`` contains the entire script body, the code -within the ``if`` statement will only be invoked when ``python -helloworld.py`` is executed from the operating system command line. - -``helloworld.py`` in this case is a Python :term:`module`. Using the ``if`` -clause is necessary -- or at least best practice -- because code in any -Python module may be imported by another Python module. By using this idiom, -the script is indicating that it does not want the code within the ``if`` -statement to execute if this module is imported; the code within the ``if`` -block should only be run during a direct script execution. +containing this code is run directly from the operating system command line. +For example, if the file named ``helloworld.py`` contains the entire script +body, the code within the ``if`` statement will only be invoked when ``python +helloworld.py`` is executed from the command line. + +Using the ``if`` clause is necessary—or at least best practice—because code in +a Python ``.py`` file may be eventually imported via the Python ``import`` +statement by another ``.py`` file. ``.py`` files that are imported by other +``.py`` files are referred to as *modules*. By using the ``if __name__ == +'__main__':`` idiom, the script above is indicating that it does not want the +code within the ``if`` statement to execute if this module is imported from +another; the code within the ``if`` block should only be run during a direct +script execution. The ``config = Configurator()`` line above creates an instance of the :class:`~pyramid.config.Configurator` class. The resulting ``config`` object represents an API which the script uses to configure this particular :app:`Pyramid` application. Methods called on the Configurator will cause -registrations to be made in a :term:`application registry` associated with -the application. +registrations to be made in an :term:`application registry` associated with the +application. .. _adding_configuration: Adding Configuration ~~~~~~~~~~~~~~~~~~~~ -.. ignore-next-block -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: + :lines: 11-12 + +The first line above calls the :meth:`pyramid.config.Configurator.add_route` +method, which registers a :term:`route` to match any URL path that begins with +``/hello/`` followed by a string. - config.add_view(hello_world) - config.add_view(goodbye_world, name='goodbye') - -Each of these lines calls the :meth:`pyramid.config.Configurator.add_view` -method. The ``add_view`` method of a configurator registers a :term:`view -configuration` within the :term:`application registry`. A :term:`view -configuration` represents a set of circumstances related to the -:term:`request` that will cause a specific :term:`view callable` to be -invoked. This "set of circumstances" is provided as one or more keyword -arguments to the ``add_view`` method. Each of these keyword arguments is -known as a view configuration :term:`predicate`. - -The line ``config.add_view(hello_world)`` registers the ``hello_world`` -function as a view callable. The ``add_view`` method of a Configurator must -be called with a view callable object or a :term:`dotted Python name` as its -first argument, so the first argument passed is the ``hello_world`` function. -This line calls ``add_view`` with a *default* value for the :term:`predicate` -argument, named ``name``. The ``name`` predicate defaults to a value -equalling the empty string (``''``). This means that we're instructing -:app:`Pyramid` to invoke the ``hello_world`` view callable when the -:term:`view name` is the empty string. We'll learn in later chapters what a -:term:`view name` is, and under which circumstances a request will have a -view name that is the empty string; in this particular application, it means -that the ``hello_world`` view callable will be invoked when the root URL -``/`` is visited by a browser. - -The line ``config.add_view(goodbye_world, name='goodbye')`` registers the -``goodbye_world`` function as a view callable. The line calls ``add_view`` -with the view callable as the first required positional argument, and a -:term:`predicate` keyword argument ``name`` with the value ``'goodbye'``. -The ``name`` argument supplied in this :term:`view configuration` implies -that only a request that has a :term:`view name` of ``goodbye`` should cause -the ``goodbye_world`` view callable to be invoked. In this particular -application, this means that the ``goodbye_world`` view callable will be -invoked when the URL ``/goodbye`` is visited by a browser. - -Each invocation of the ``add_view`` method registers a :term:`view -configuration`. Each :term:`predicate` provided as a keyword argument to the -``add_view`` method narrows the set of circumstances which would cause the -view configuration's callable to be invoked. In general, a greater number of -predicates supplied along with a view configuration will more strictly limit -the applicability of its associated view callable. When :app:`Pyramid` -processes a request, the view callable with the *most specific* view -configuration (the view configuration that matches the most specific set of -predicates) is always invoked. - -In this application, :app:`Pyramid` chooses the most specific view callable -based only on view :term:`predicate` applicability. The ordering of calls to -:meth:`~pyramid.config.Configurator.add_view` is never very important. We can -register ``goodbye_world`` first and ``hello_world`` second; :app:`Pyramid` -will still give us the most specific callable when a request is dispatched to -it. +The second line registers the ``hello_world`` function as a :term:`view +callable` and makes sure that it will be called when the ``hello`` route is +matched. .. index:: single: make_wsgi_app @@ -252,74 +184,67 @@ it. WSGI Application Creation ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. ignore-next-block -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: - - app = config.make_wsgi_app() + :lines: 13 After configuring views and ending configuration, the script creates a WSGI -*application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` -method. A call to ``make_wsgi_app`` implies that all configuration is -finished (meaning all method calls to the configurator which set up views, -and various other configuration settings have been performed). The -``make_wsgi_app`` method returns a :term:`WSGI` application object that can -be used by any WSGI server to present an application to a requestor. -:term:`WSGI` is a protocol that allows servers to talk to Python -applications. We don't discuss :term:`WSGI` in any depth within this book, -however, you can learn more about it by visiting `wsgi.org -<http://wsgi.org>`_. - -The :app:`Pyramid` application object, in particular, is an instance of a -class representing a :app:`Pyramid` :term:`router`. It has a reference to -the :term:`application registry` which resulted from method calls to the -configurator used to configure it. The :term:`router` consults the registry -to obey the policy choices made by a single application. These policy -choices were informed by method calls to the :term:`Configurator` made -earlier; in our case, the only policy choices made were implied by two calls -to its ``add_view`` method. +*application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` method. +A call to ``make_wsgi_app`` implies that all configuration is finished +(meaning all method calls to the configurator, which sets up views and various +other configuration settings, have been performed). The ``make_wsgi_app`` +method returns a :term:`WSGI` application object that can be used by any WSGI +server to present an application to a requestor. :term:`WSGI` is a protocol +that allows servers to talk to Python applications. We don't discuss +:term:`WSGI` in any depth within this book, but you can learn more about it by +visiting `wsgi.org <http://wsgi.org>`_. + +The :app:`Pyramid` application object, in particular, is an instance of a class +representing a :app:`Pyramid` :term:`router`. It has a reference to the +:term:`application registry` which resulted from method calls to the +configurator used to configure it. The :term:`router` consults the registry to +obey the policy choices made by a single application. These policy choices +were informed by method calls to the :term:`Configurator` made earlier; in our +case, the only policy choices made were implied by calls to its ``add_view`` +and ``add_route`` methods. WSGI Application Serving ~~~~~~~~~~~~~~~~~~~~~~~~ -.. ignore-next-block -.. code-block:: python +.. literalinclude:: helloworld.py :linenos: - - serve(app, host='0.0.0.0') - -Finally, we actually serve the application to requestors by starting up a -WSGI server. We happen to use the :func:`paste.httpserver.serve` WSGI server -runner, passing it the ``app`` object (a :term:`router`) as the application -we wish to serve. We also pass in an argument ``host=='0.0.0.0'``, meaning -"listen on all TCP interfaces." By default, the Paste HTTP server listens -only on the ``127.0.0.1`` interface, which is problematic if you're running -the server on a remote system and you wish to access it with a web browser -from a local system. We don't specify a TCP port number to listen on; this -means we want to use the default TCP port, which is 8080. - -When this line is invoked, it causes the server to start listening on TCP -port 8080. It will serve requests forever, or at least until we stop it by -killing the process which runs it (usually by pressing ``Ctrl-C`` in the -terminal we used to start it). + :lines: 14-15 + +Finally, we actually serve the application to requestors by starting up a WSGI +server. We happen to use the :mod:`wsgiref` ``make_server`` server maker for +this purpose. We pass in as the first argument ``'0.0.0.0'``, which means +"listen on all TCP interfaces". By default, the HTTP server listens only on +the ``127.0.0.1`` interface, which is problematic if you're running the server +on a remote system and you wish to access it with a web browser from a local +system. We also specify a TCP port number to listen on, which is 8080, passing +it as the second argument. The final argument is the ``app`` object (a +:term:`router`), which is the application we wish to serve. Finally, we call +the server's ``serve_forever`` method, which starts the main loop in which it +will wait for requests from the outside world. + +When this line is invoked, it causes the server to start listening on TCP port +8080. The server will serve requests forever, or at least until we stop it by +killing the process which runs it (usually by pressing ``Ctrl-C`` or +``Ctrl-Break`` in the terminal we used to start it). Conclusion ~~~~~~~~~~ Our hello world application is one of the simplest possible :app:`Pyramid` applications, configured "imperatively". We can see that it's configured -imperatively because the full power of Python is available to us as we -perform configuration tasks. +imperatively because the full power of Python is available to us as we perform +configuration tasks. References ---------- -For more information about the API of a :term:`Configurator` object, -see :class:`~pyramid.config.Configurator` . +For more information about the API of a :term:`Configurator` object, see +:class:`~pyramid.config.Configurator` . For more information about :term:`view configuration`, see :ref:`view_config_chapter`. - -An example of using *declarative* configuration (:term:`ZCML`) instead of -imperative configuration to create a similar "hello world" is available -within the documentation for :term:`pyramid_zcml`. diff --git a/docs/narr/hellotraversal.py b/docs/narr/hellotraversal.py new file mode 100644 index 000000000..1ef7525e6 --- /dev/null +++ b/docs/narr/hellotraversal.py @@ -0,0 +1,22 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + +class Resource(dict): + pass + +def get_root(request): + return Resource({'a': Resource({'b': Resource({'c': Resource()})})}) + +def hello_world_of_resources(context, request): + output = "Here's a resource and its children: %s" % context + return Response(output) + +if __name__ == '__main__': + config = Configurator(root_factory=get_root) + config.add_view(hello_world_of_resources, context=Resource) + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() + + diff --git a/docs/narr/hellotraversal.rst b/docs/narr/hellotraversal.rst new file mode 100644 index 000000000..543e2171f --- /dev/null +++ b/docs/narr/hellotraversal.rst @@ -0,0 +1,64 @@ +.. _hello_traversal_chapter: + +Hello Traversal World +===================== + +.. index:: + single: traversal quick example + +Traversal is an alternative to URL dispatch which allows Pyramid applications +to map URLs to code. + +If code speaks louder than words, maybe this will help. Here is a single-file +Pyramid application that uses traversal: + +.. literalinclude:: hellotraversal.py + :linenos: + +You may notice that this application is intentionally very similar to the +"hello world" application from :doc:`firstapp`. + +On lines 5-6, we create a trivial :term:`resource` class that's just a +dictionary subclass. + +On lines 8-9, we hard-code a :term:`resource tree` in our :term:`root factory` +function. + +On lines 11-13, we define a single :term:`view callable` that can display a +single instance of our ``Resource`` class, passed as the ``context`` argument. + +The rest of the file sets up and serves our :app:`Pyramid` WSGI app. Line 18 +is where our view gets configured for use whenever the traversal ends with an +instance of our ``Resource`` class. + +Interestingly, there are no URLs explicitly configured in this application. +Instead, the URL space is defined entirely by the keys in the resource tree. + +Example requests +---------------- + +If this example is running on http://localhost:8080, and the user browses to +http://localhost:8080/a/b, Pyramid will call ``get_root(request)`` to get the +root resource, then traverse the tree from there by key; starting from the +root, it will find the child with key ``"a"``, then its child with key ``"b"``; +then use that as the ``context`` argument for calling +``hello_world_of_resources``. + +Or, if the user browses to http://localhost:8080/, Pyramid will stop at the +root—the outermost ``Resource`` instance, in this case—and use that as the +``context`` argument to the same view. + +Or, if the user browses to a key that doesn't exist in this resource tree, like +http://localhost:8080/xyz or http://localhost:8080/a/b/c/d, the traversal will +end by raising a KeyError, and Pyramid will turn that into a 404 HTTP response. + +A more complicated application could have many types of resources, with +different view callables defined for each type, and even multiple views for +each type. + +.. seealso:: + + Full technical details may be found in :doc:`traversal`. + + For more about *why* you might use traversal, see + :doc:`muchadoabouttraversal`. diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py new file mode 100644 index 000000000..c01329af9 --- /dev/null +++ b/docs/narr/helloworld.py @@ -0,0 +1,16 @@ +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() + diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index be139ad74..b776f99e8 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -14,61 +14,122 @@ in various ways. Changing the Not Found View --------------------------- -When :app:`Pyramid` can't map a URL to view code, it invokes a :term:`not -found view`, which is a :term:`view callable`. A default notfound view -exists. The default not found view can be overridden through application -configuration. +When :app:`Pyramid` can't map a URL to view code, it invokes a :term:`Not Found +View`, which is a :term:`view callable`. The default Not Found View can be +overridden through application configuration. + +If your application uses :term:`imperative configuration`, you can replace the +Not Found View by using the +:meth:`pyramid.config.Configurator.add_notfound_view` method: + +.. code-block:: python + :linenos: -The :term:`not found view` callable is a view callable like any other. The -:term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.exceptions.NotFound` class as the -``context`` of the view configuration. + def notfound(request): + return Response('Not Found, dude', status='404 Not Found') -If your application uses :term:`imperative configuration`, you can replace -the Not Found view by using the :meth:`pyramid.config.Configurator.add_view` -method to register an "exception view": + def main(globals, **settings): + config = Configurator() + config.add_notfound_view(notfound) + +The :term:`Not Found View` callable is a view callable like any other. + +If your application instead uses :class:`pyramid.view.view_config` decorators +and a :term:`scan`, you can replace the Not Found View by using the +:class:`pyramid.view.notfound_view_config` decorator: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound - from helloworld.views import notfound_view - config.add_view(notfound_view, context=NotFound) + from pyramid.view import notfound_view_config + + @notfound_view_config() + def notfound(request): + return Response('Not Found, dude', status='404 Not Found') -Replace ``helloworld.views.notfound_view`` with a reference to the -:term:`view callable` you want to use to represent the Not Found view. + def main(globals, **settings): + config = Configurator() + config.scan() -Like any other view, the notfound view must accept at least a ``request`` -parameter, or both ``context`` and ``request``. The ``request`` is the -current :term:`request` representing the denied action. The ``context`` (if -used in the call signature) will be the instance of the -:exc:`~pyramid.exceptions.NotFound` exception that caused the view to be -called. +This does exactly what the imperative example above showed. -Here's some sample code that implements a minimal NotFound view callable: +Your application can define *multiple* Not Found Views if necessary. Both +:meth:`pyramid.config.Configurator.add_notfound_view` and +:class:`pyramid.view.notfound_view_config` take most of the same arguments as +:class:`pyramid.config.Configurator.add_view` and +:class:`pyramid.view.view_config`, respectively. This means that Not Found +Views can carry predicates limiting their applicability. For example: + +.. code-block:: python + :linenos: + + from pyramid.view import notfound_view_config + + @notfound_view_config(request_method='GET') + def notfound_get(request): + return Response('Not Found during GET, dude', status='404 Not Found') + + @notfound_view_config(request_method='POST') + def notfound_post(request): + return Response('Not Found during POST, dude', status='404 Not Found') + + def main(globals, **settings): + config = Configurator() + config.scan() + +The ``notfound_get`` view will be called when a view could not be found and the +request method was ``GET``. The ``notfound_post`` view will be called when a +view could not be found and the request method was ``POST``. + +Like any other view, the Not Found View must accept at least a ``request`` +parameter, or both ``context`` and ``request``. The ``request`` is the current +:term:`request` representing the denied action. The ``context`` (if used in +the call signature) will be the instance of the +:exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the view to +be called. + +Both :meth:`pyramid.config.Configurator.add_notfound_view` and +:class:`pyramid.view.notfound_view_config` can be used to automatically +redirect requests to slash-appended routes. See +:ref:`redirecting_to_slash_appended_routes` for examples. + +Here's some sample code that implements a minimal :term:`Not Found View` +callable: .. code-block:: python :linenos: from pyramid.httpexceptions import HTTPNotFound - def notfound_view(request): + def notfound(request): return HTTPNotFound() -.. note:: When a NotFound view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.NotFound` - exception that caused the not found view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - not found error was raised. This message will be different when - the ``debug_notfound`` environment setting is true than it is when - it is false. - -.. warning:: When a NotFound view callable accepts an argument list as - described in :ref:`request_and_context_view_definitions`, the ``context`` - passed as the first argument to the view callable will be the - :exc:`~pyramid.exceptions.NotFound` exception instance. If available, the - resource context will still be available as ``request.context``. +.. note:: + + When a Not Found View callable is invoked, it is passed a :term:`request`. + The ``exception`` attribute of the request will be an instance of the + :exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the Not + Found View to be called. The value of ``request.exception.message`` will be + a value explaining why the Not Found exception was raised. This message has + different values depending on whether the ``pyramid.debug_notfound`` + environment setting is true or false. + +.. note:: + + Both :meth:`pyramid.config.Configurator.add_notfound_view` and + :class:`pyramid.view.notfound_view_config` are new as of Pyramid 1.3. + Older Pyramid documentation instructed users to use ``add_view`` instead, + with a ``context`` of ``HTTPNotFound``. This still works; the convenience + method and decorator are just wrappers around this functionality. + +.. warning:: + + When a Not Found View callable accepts an argument list as described in + :ref:`request_and_context_view_definitions`, the ``context`` passed as the + first argument to the view callable will be the + :exc:`~pyramid.httpexceptions.HTTPNotFound` exception instance. If + available, the resource context will still be available as + ``request.context``. .. index:: single: forbidden view @@ -79,55 +140,75 @@ Changing the Forbidden View --------------------------- When :app:`Pyramid` can't authorize execution of a view based on the -:term:`authorization policy` in use, it invokes a :term:`forbidden view`. -The default forbidden response has a 403 status code and is very plain, but -the view which generates it can be overridden as necessary. +:term:`authorization policy` in use, it invokes a :term:`forbidden view`. The +default forbidden response has a 403 status code and is very plain, but the +view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The -:term:`view configuration` which causes it to be a "forbidden" view consists -only of naming the :exc:`pyramid.exceptions.Forbidden` class as the -``context`` of the view configuration. +:term:`view configuration` which causes it to be a "forbidden" view consists of +using the :meth:`pyramid.config.Configurator.add_forbidden_view` API or the +:class:`pyramid.view.forbidden_view_config` decorator. + +For example, you can add a forbidden view by using the +:meth:`pyramid.config.Configurator.add_forbidden_view` method to register a +forbidden view: + +.. code-block:: python + :linenos: -You can replace the forbidden view by using the -:meth:`pyramid.config.Configurator.add_view` method to register an "exception -view": + def forbidden(request): + return Response('forbidden') + + def main(globals, **settings): + config = Configurator() + config.add_forbidden_view(forbidden_view) + +If instead you prefer to use decorators and a :term:`scan`, you can use the +:class:`pyramid.view.forbidden_view_config` decorator to mark a view callable +as a forbidden view: .. code-block:: python :linenos: - from helloworld.views import forbidden_view - from pyramid.exceptions import Forbidden - config.add_view(forbidden_view, context=Forbidden) + from pyramid.view import forbidden_view_config + + @forbidden_view_config() + def forbidden(request): + return Response('forbidden') -Replace ``helloworld.views.forbidden_view`` with a reference to the Python -:term:`view callable` you want to use to represent the Forbidden view. + def main(globals, **settings): + config = Configurator() + config.scan() Like any other view, the forbidden view must accept at least a ``request`` -parameter, or both ``context`` and ``request``. The ``context`` (available -as ``request.context`` if you're using the request-only view argument -pattern) is the context found by the router when the view invocation was -denied. The ``request`` is the current :term:`request` representing the -denied action. +parameter, or both ``context`` and ``request``. If a forbidden view callable +accepts both ``context`` and ``request``, the HTTP Exception is passed as +context. The ``context`` as found by the router when the view was denied (which +you normally would expect) is available as ``request.context``. The +``request`` is the current :term:`request` representing the denied action. Here's some sample code that implements a minimal forbidden view: .. code-block:: python :linenos: - from pyramid.views import view_config + from pyramid.view import view_config from pyramid.response import Response def forbidden_view(request): return Response('forbidden') -.. note:: When a forbidden view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.Forbidden` - exception that caused the forbidden view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - forbidden was raised. This message will be different when the - ``debug_authorization`` environment setting is true than it is when - it is false. +.. note:: + + When a forbidden view callable is invoked, it is passed a :term:`request`. + The ``exception`` attribute of the request will be an instance of the + :exc:`~pyramid.httpexceptions.HTTPForbidden` exception that caused the + forbidden view to be called. The value of ``request.exception.message`` + will be a value explaining why the forbidden exception was raised, and + ``request.exception.result`` will be extended information about the + forbidden exception. These messages have different values depending on + whether the ``pyramid.debug_authorization`` environment setting is true or + false. .. index:: single: request factory @@ -137,13 +218,13 @@ Here's some sample code that implements a minimal forbidden view: Changing the Request Factory ---------------------------- -Whenever :app:`Pyramid` handles a :term:`WSGI` request, it creates a -:term:`request` object based on the WSGI environment it has been passed. By -default, an instance of the :class:`pyramid.request.Request` class is created -to represent the request object. +Whenever :app:`Pyramid` handles a request from a :term:`WSGI` server, it +creates a :term:`request` object based on the WSGI environment it has been +passed. By default, an instance of the :class:`pyramid.request.Request` class +is created to represent the request object. -The class (aka "factory") that :app:`Pyramid` uses to create a request object -instance can be changed by passing a ``request_factory`` argument to the +The class (a.k.a., "factory") that :app:`Pyramid` uses to create a request +object instance can be changed by passing a ``request_factory`` argument to the constructor of the :term:`configurator`. This argument can be either a callable or a :term:`dotted Python name` representing a callable. @@ -158,7 +239,7 @@ callable or a :term:`dotted Python name` representing a callable. config = Configurator(request_factory=MyRequest) If you're doing imperative configuration, and you'd rather do it after you've -already constructed a :term:`configurator` it can also be registered via the +already constructed a :term:`configurator`, it can also be registered via the :meth:`pyramid.config.Configurator.set_request_factory` method: .. code-block:: python @@ -174,75 +255,162 @@ already constructed a :term:`configurator` it can also be registered via the config.set_request_factory(MyRequest) .. index:: - single: renderer globals + single: request method + +.. _adding_request_method: + +Adding Methods or Properties to a Request Object +------------------------------------------------ + +.. versionadded:: 1.4 -.. _adding_renderer_globals: +Since each Pyramid application can only have one :term:`request` factory, +:ref:`changing the request factory <changing_the_request_factory>` is not that +extensible, especially if you want to build composable features (e.g., Pyramid +add-ons and plugins). -Adding Renderer Globals ------------------------ +A lazy property can be registered to the request object via the +:meth:`pyramid.config.Configurator.add_request_method` API. This allows you to +specify a callable that will be available on the request object, but will not +actually execute the function until accessed. -Whenever :app:`Pyramid` handles a request to perform a rendering (after a -view with a ``renderer=`` configuration attribute is invoked, or when any -of the methods beginning with ``render`` within the :mod:`pyramid.renderers` -module are called), *renderer globals* can be injected into the *system* -values sent to the renderer. By default, no renderer globals are injected, -and the "bare" system values (such as ``request``, ``context``, and -``renderer_name``) are the only values present in the system dictionary -passed to every renderer. +.. warning:: -A callback that :app:`Pyramid` will call every time a renderer is invoked can -be added by passing a ``renderer_globals_factory`` argument to the -constructor of the :term:`configurator`. This callback can either be a -callable object or a :term:`dotted Python name` representing such a callable. + This will silently override methods and properties from :term:`request + factory` that have the same name. .. code-block:: python :linenos: - def renderer_globals_factory(system): - return {'a': 1} + from pyramid.config import Configurator - config = Configurator( - renderer_globals_factory=renderer_globals_factory) + def total(request, *args): + return sum(args) -Such a callback must accept a single positional argument (notionally named -``system``) which will contain the original system values. It must return a -dictionary of values that will be merged into the system dictionary. See -:ref:`renderer_system_values` for description of the values present in the -system dictionary. + def prop(request): + print("getting the property") + return "the property" -If you're doing imperative configuration, and you'd rather do it after you've -already constructed a :term:`configurator` it can also be registered via the -:meth:`pyramid.config.Configurator.set_renderer_globals_factory` method: + config = Configurator() + config.add_request_method(total) + config.add_request_method(prop, reify=True) + +In the above example, ``total`` is added as a method. However, ``prop`` is +added as a property and its result is cached per-request by setting +``reify=True``. This way, we eliminate the overhead of running the function +multiple times. + + >>> request.total(1, 2, 3) + 6 + >>> request.prop + getting the property + the property + >>> request.prop + the property + +To not cache the result of ``request.prop``, set ``property=True`` instead of +``reify=True``. + +Here is an example of passing a class to ``Configurator.add_request_method``: .. code-block:: python :linenos: from pyramid.config import Configurator + from pyramid.decorator import reify + + class ExtraStuff(object): + + def __init__(self, request): + self.request = request + + def total(self, *args): + return sum(args) - def renderer_globals_factory(system): - return {'a': 1} + # use @property if you don't want to cache the result + @reify + def prop(self): + print("getting the property") + return "the property" config = Configurator() - config.set_renderer_globals_factory(renderer_globals_factory) + config.add_request_method(ExtraStuff, 'extra', reify=True) + +We attach and cache an object named ``extra`` to the ``request`` object. + + >>> request.extra.total(1, 2, 3) + 6 + >>> request.extra.prop + getting the property + the property + >>> request.extra.prop + the property + +.. index:: + single: response factory + +.. _changing_the_response_factory: + +Changing the Response Factory +----------------------------- + +.. versionadded:: 1.6 + +Whenever :app:`Pyramid` returns a response from a view, it creates a +:term:`response` object. By default, an instance of the +:class:`pyramid.response.Response` class is created to represent the response +object. + +The factory that :app:`Pyramid` uses to create a response object instance can +be changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument +to the constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. + +The factory takes a single positional argument, which is a :term:`Request` +object. The argument may be ``None``. + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + class MyResponse(Response): + pass + + config = Configurator(response_factory=lambda r: MyResponse()) -Another mechanism which allows event subscribers to add renderer global values -exists in :ref:`beforerender_event`. +If you're doing imperative configuration and you'd rather do it after you've +already constructed a :term:`configurator`, it can also be registered via the +:meth:`pyramid.config.Configurator.set_response_factory` method: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + from pyramid.response import Response + + class MyResponse(Response): + pass + + config = Configurator() + config.set_response_factory(lambda r: MyResponse()) .. index:: single: before render event + single: adding renderer globals .. _beforerender_event: -Using The Before Render Event +Using the Before Render Event ----------------------------- Subscribers to the :class:`pyramid.events.BeforeRender` event may introspect and modify the set of :term:`renderer globals` before they are passed to a -:term:`renderer`. This event object iself has a dictionary-like interface -that can be used for this purpose. For example: +:term:`renderer`. This event object iself has a dictionary-like interface that +can be used for this purpose. For example: .. code-block:: python - :linenos: + :linenos: from pyramid.events import subscriber from pyramid.events import BeforeRender @@ -252,23 +420,48 @@ that can be used for this purpose. For example: event['mykey'] = 'foo' An object of this type is sent as an event just before a :term:`renderer` is -invoked (but *after* the application-level renderer globals factory added via -:class:`~pyramid.config.Configurator.set_renderer_globals_factory`, if any, -has injected its own keys into the renderer globals dictionary). +invoked. -If a subscriber attempts to add a key that already exist in the renderer +If a subscriber attempts to add a key that already exists in the renderer globals dictionary, a :exc:`KeyError` is raised. This limitation is enforced because event subscribers do not possess any relative ordering. The set of keys added to the renderer globals dictionary by all -:class:`pyramid.events.BeforeRender` subscribers and renderer globals -factories must be unique. +:class:`pyramid.events.BeforeRender` subscribers and renderer globals factories +must be unique. + +The dictionary returned from the view is accessible through the +:attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` +event. + +Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from your +view callable, like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + +:attr:`rendering_val` can be used to access these values from the +:class:`~pyramid.events.BeforeRender` object: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) See the API documentation for the :class:`~pyramid.events.BeforeRender` event interface at :class:`pyramid.interfaces.IBeforeRender`. -Another mechanism which allows event subscribers more control when adding -renderer global values exists in :ref:`adding_renderer_globals`. - .. index:: single: response callback @@ -279,8 +472,8 @@ Using Response Callbacks Unlike many other web frameworks, :app:`Pyramid` does not eagerly create a global response object. Adding a :term:`response callback` allows an -application to register an action to be performed against a response object -once it is created, usually in order to mutate it. +application to register an action to be performed against whatever response +object is returned by a view, usually in order to mutate the response. The :meth:`pyramid.request.Request.add_response_callback` method is used to register a response callback. @@ -297,24 +490,23 @@ A response callback is a callable which accepts two positional parameters: response.cache_control.max_age = 360 request.add_response_callback(cache_callback) -No response callback is called if an unhandled exception happens in -application code, or if the response object returned by a :term:`view -callable` is invalid. Response callbacks *are*, however, invoked when a -:term:`exception view` is rendered successfully: in such a case, the -:attr:`request.exception` attribute of the request when it enters a response -callback will be an exception object instead of its default value of -``None``. +No response callback is called if an unhandled exception happens in application +code, or if the response object returned by a :term:`view callable` is invalid. +Response callbacks *are*, however, invoked when a :term:`exception view` is +rendered successfully. In such a case, the :attr:`request.exception` attribute +of the request when it enters a response callback will be an exception object +instead of its default value of ``None``. Response callbacks are called in the order they're added -(first-to-most-recently-added). All response callbacks are called *after* -the :class:`~pyramid.events.NewResponse` event is sent. Errors raised by -response callbacks are not handled specially. They will be propagated to the -caller of the :app:`Pyramid` router application. +(first-to-most-recently-added). All response callbacks are called *before* the +:class:`~pyramid.events.NewResponse` event is sent. Errors raised by response +callbacks are not handled specially. They will be propagated to the caller of +the :app:`Pyramid` router application. A response callback has a lifetime of a *single* request. If you want a response callback to happen as the result of *every* request, you must -re-register the callback into every new request (perhaps within a subscriber -of a :class:`~pyramid.events.NewRequest` event). +re-register the callback into every new request (perhaps within a subscriber of +a :class:`~pyramid.events.NewRequest` event). .. index:: single: finished callback @@ -325,59 +517,50 @@ Using Finished Callbacks ------------------------ A :term:`finished callback` is a function that will be called unconditionally -by the :app:`Pyramid` :term:`router` at the very end of request processing. -A finished callback can be used to perform an action at the end of a request +by the :app:`Pyramid` :term:`router` at the very end of request processing. A +finished callback can be used to perform an action at the end of a request unconditionally. The :meth:`pyramid.request.Request.add_finished_callback` method is used to register a finished callback. -A finished callback is a callable which accepts a single positional -parameter: ``request``. For example: +A finished callback is a callable which accepts a single positional parameter: +``request``. For example: .. code-block:: python :linenos: - import transaction + import logging - def commit_callback(request): - '''commit or abort the transaction associated with request''' - if request.exception is not None: - transaction.abort() - else: - transaction.commit() - request.add_finished_callback(commit_callback) + log = logging.getLogger(__name__) + + def log_callback(request): + """Log information at the end of request""" + log.debug('Request is finished.') + request.add_finished_callback(log_callback) Finished callbacks are called in the order they're added -(first-to-most-recently-added). Finished callbacks (unlike a -:term:`response callback`) are *always* called, even if an exception -happens in application code that prevents a response from being -generated. - -The set of finished callbacks associated with a request are called *very -late* in the processing of that request; they are essentially the very last -thing called by the :term:`router` before a request "ends". They are called -after response processing has already occurred in a top-level ``finally:`` -block within the router request processing code. As a result, mutations -performed to the ``request`` provided to a finished callback will have no -meaningful effect, because response processing will have already occurred, -and the request's scope will expire almost immediately after all finished -callbacks have been processed. - -It is often necessary to tell whether an exception occurred within -:term:`view callable` code from within a finished callback: in such a case, -the :attr:`request.exception` attribute of the request when it enters a -response callback will be an exception object instead of its default value of -``None``. - -Errors raised by finished callbacks are not handled specially. They -will be propagated to the caller of the :app:`Pyramid` router -application. +(first-to-most-recently-added). Finished callbacks (unlike a :term:`response +callback`) are *always* called, even if an exception happens in application +code that prevents a response from being generated. + +The set of finished callbacks associated with a request are called *very late* +in the processing of that request; they are essentially the very last thing +called by the :term:`router` before a request "ends". They are called after +response processing has already occurred in a top-level ``finally:`` block +within the router request processing code. As a result, mutations performed to +the ``request`` provided to a finished callback will have no meaningful effect, +because response processing will have already occurred, and the request's scope +will expire almost immediately after all finished callbacks have been +processed. + +Errors raised by finished callbacks are not handled specially. They will be +propagated to the caller of the :app:`Pyramid` router application. A finished callback has a lifetime of a *single* request. If you want a finished callback to happen as the result of *every* request, you must -re-register the callback into every new request (perhaps within a subscriber -of a :class:`~pyramid.events.NewRequest` event). +re-register the callback into every new request (perhaps within a subscriber of +a :class:`~pyramid.events.NewRequest` event). .. index:: single: traverser @@ -389,17 +572,16 @@ Changing the Traverser The default :term:`traversal` algorithm that :app:`Pyramid` uses is explained in :ref:`traversal_algorithm`. Though it is rarely necessary, this default -algorithm can be swapped out selectively for a different traversal pattern -via configuration. +algorithm can be swapped out selectively for a different traversal pattern via +configuration. .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface + from pyramid.config import Configurator from myapp.traversal import Traverser - - config.registry.registerAdapter(Traverser, (Interface,), ITraverser) + config = Configurator() + config.add_traverser(Traverser) In the example above, ``myapp.traversal.Traverser`` is assumed to be a class that implements the following interface: @@ -437,90 +619,208 @@ that implements the following interface: More than one traversal algorithm can be active at the same time. For instance, if your :term:`root factory` returns more than one type of object -conditionally, you could claim that an alternate traverser adapter is ``for`` +conditionally, you could claim that an alternative traverser adapter is "for" only one particular class or interface. When the root factory returned an object that implemented that class or interface, a custom traverser would be -used. Otherwise, the default traverser would be used. For example: +used. Otherwise the default traverser would be used. For example: .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface from myapp.traversal import Traverser from myapp.resources import MyRoot - - config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) + from pyramid.config import Configurator + config = Configurator() + config.add_traverser(Traverser, MyRoot) If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` -function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only -when the application :term:`root factory` returned an instance of the +function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only when +the application :term:`root factory` returned an instance of the ``myapp.resources.MyRoot`` object. Otherwise it would use the default :app:`Pyramid` traverser to do traversal. .. index:: - single: url generator + single: URL generator .. _changing_resource_url: -Changing How :mod:`pyramid.url.resource_url` Generates a URL ------------------------------------------------------------- +Changing How :meth:`pyramid.request.Request.resource_url` Generates a URL +------------------------------------------------------------------------- When you add a traverser as described in :ref:`changing_the_traverser`, it's -often convenient to continue to use the :func:`pyramid.url.resource_url` API. -However, since the way traversal is done will have been modified, the URLs it -generates by default may be incorrect. +often convenient to continue to use the +:meth:`pyramid.request.Request.resource_url` API. However, since the way +traversal is done will have been modified, the URLs it generates by default may +be incorrect when used against resources derived from your custom traverser. If you've added a traverser, you can change how -:func:`~pyramid.url.resource_url` generates a URL for a specific type of -resource by adding a registerAdapter call for -:class:`pyramid.interfaces.IContextURL` to your application: +:meth:`~pyramid.request.Request.resource_url` generates a URL for a specific +type of resource by adding a call to +:meth:`pyramid.config.Configurator.add_resource_url_adapter`. + +For example: .. code-block:: python :linenos: - from pyramid.interfaces import ITraverser - from zope.interface import Interface - from myapp.traversal import URLGenerator + from myapp.traversal import ResourceURLAdapter from myapp.resources import MyRoot - config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), - IContextURL) + config.add_resource_url_adapter(ResourceURLAdapter, MyRoot) -In the above example, the ``myapp.traversal.URLGenerator`` class will be used -to provide services to :func:`~pyramid.url.resource_url` any time the -:term:`context` passed to ``resource_url`` is of class -``myapp.resources.MyRoot``. The second argument in the ``(MyRoot, -Interface)`` tuple represents the type of interface that must be possessed by -the :term:`request` (in this case, any interface, represented by -``zope.interface.Interface``). +In the above example, the ``myapp.traversal.ResourceURLAdapter`` class will be +used to provide services to :meth:`~pyramid.request.Request.resource_url` any +time the :term:`resource` passed to ``resource_url`` is of the class +``myapp.resources.MyRoot``. The ``resource_iface`` argument ``MyRoot`` +represents the type of interface that must be possessed by the resource for +this resource url factory to be found. If the ``resource_iface`` argument is +omitted, this resource URL adapter will be used for *all* resources. The API that must be implemented by a class that provides -:class:`~pyramid.interfaces.IContextURL` is as follows: +:class:`~pyramid.interfaces.IResourceURL` is as follows: .. code-block:: python :linenos: - from zope.interface import Interface - - class IContextURL(Interface): - """ An adapter which deals with URLs related to a context. + class MyResourceURL(object): + """ An adapter which provides the virtual and physical paths of a + resource """ - def __init__(self, context, request): - """ Accept the context and request """ - - def virtual_root(self): - """ Return the virtual root object related to a request and the - current context""" - - def __call__(self): - """ Return a URL that points to the context """ + def __init__(self, resource, request): + """ Accept the resource and request and set self.physical_path and + self.virtual_path """ + self.virtual_path = some_function_of(resource, request) + self.physical_path = some_other_function_of(resource, request) The default context URL generator is available for perusal as the class -:class:`pyramid.traversal.TraversalContextURL` in the `traversal module -<http://github.com/Pylons/pyramid/blob/master/pyramid/traversal.py>`_ of the +:class:`pyramid.traversal.ResourceURL` in the `traversal module +<https://github.com/Pylons/pyramid/blob/master/pyramid/traversal.py>`_ of the :term:`Pylons` GitHub Pyramid repository. +See :meth:`pyramid.config.Configurator.add_resource_url_adapter` for more +information. + +.. index:: + single: IResponse + single: special view responses + +.. _using_iresponse: + +Changing How Pyramid Treats View Responses +------------------------------------------ + +.. versionadded:: 1.1 + +It is possible to control how Pyramid treats the result of calling a view +callable on a per-type basis by using a hook involving +:meth:`pyramid.config.Configurator.add_response_adapter` or the +:class:`~pyramid.response.response_adapter` decorator. + +Pyramid, in various places, adapts the result of calling a view callable to the +:class:`~pyramid.interfaces.IResponse` interface to ensure that the object +returned by the view callable is a "true" response object. The vast majority +of time, the result of this adaptation is the result object itself, as view +callables written by "civilians" who read the narrative documentation contained +in this manual will always return something that implements the +:class:`~pyramid.interfaces.IResponse` interface. Most typically, this will be +an instance of the :class:`pyramid.response.Response` class or a subclass. If a +civilian returns a non-Response object from a view callable that isn't +configured to use a :term:`renderer`, they will typically expect the router to +raise an error. However, you can hook Pyramid in such a way that users can +return arbitrary values from a view callable by providing an adapter which +converts the arbitrary return value into something that implements +:class:`~pyramid.interfaces.IResponse`. + +For example, if you'd like to allow view callables to return bare string +objects (without requiring a :term:`renderer` to convert a string to a response +object), you can register an adapter which converts the string to a Response: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def string_response_adapter(s): + response = Response(s) + return response + + # config is an instance of pyramid.config.Configurator + + config.add_response_adapter(string_response_adapter, str) + +Likewise, if you want to be able to return a simplified kind of response object +from view callables, you can use the IResponse hook to register an adapter to +the more complex IResponse interface: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + class SimpleResponse(object): + def __init__(self, body): + self.body = body + + def simple_response_adapter(simple_response): + response = Response(simple_response.body) + return response + + # config is an instance of pyramid.config.Configurator + + config.add_response_adapter(simple_response_adapter, SimpleResponse) + +If you want to implement your own Response object instead of using the +:class:`pyramid.response.Response` object in any capacity at all, you'll have +to make sure that the object implements every attribute and method outlined in +:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses +``zope.interface.implementer(IResponse)`` as a class decorator. + +.. code-block:: python + :linenos: + + from pyramid.interfaces import IResponse + from zope.interface import implementer + + @implementer(IResponse) + class MyResponse(object): + # ... an implementation of every method and attribute + # documented in IResponse should follow ... + +When an alternate response object implementation is returned by a view +callable, if that object asserts that it implements +:class:`~pyramid.interfaces.IResponse` (via +``zope.interface.implementer(IResponse)``) , an adapter needn't be registered +for the object; Pyramid will use it directly. + +An IResponse adapter for ``webob.Response`` (as opposed to +:class:`pyramid.response.Response`) is registered by Pyramid by default at +startup time, as by their nature, instances of this class (and instances of +subclasses of the class) will natively provide IResponse. The adapter +registered for ``webob.Response`` simply returns the response object. + +Instead of using :meth:`pyramid.config.Configurator.add_response_adapter`, you +can use the :class:`pyramid.response.response_adapter` decorator: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.response import response_adapter + + @response_adapter(str) + def string_response_adapter(s): + response = Response(s) + return response + +The above example, when scanned, has the same effect as: + +.. code-block:: python + + config.add_response_adapter(string_response_adapter, str) + +The :class:`~pyramid.response.response_adapter` decorator will have no effect +until activated by a :term:`scan`. + .. index:: single: view mapper @@ -535,31 +835,29 @@ callables by employing a :term:`view mapper`. A view mapper is an object that accepts a set of keyword arguments and which returns a callable. The returned callable is called with the :term:`view -callable` object. The returned callable should itself return another -callable which can be called with the "internal calling protocol" ``(context, +callable` object. The returned callable should itself return another callable +which can be called with the "internal calling protocol" ``(context, request)``. You can use a view mapper in a number of ways: -- by setting a ``__view_mapper__`` attribute (which is the view mapper - object) on the view callable itself +- by setting a ``__view_mapper__`` attribute (which is the view mapper object) + on the view callable itself -- by passing the mapper object to - :meth:`pyramid.config.Configurator.add_view` (or its declarative/decorator - equivalents) as the ``mapper`` argument. +- by passing the mapper object to :meth:`pyramid.config.Configurator.add_view` + (or its declarative and decorator equivalents) as the ``mapper`` argument -- by registering a *default* view mapper. +- by registering a *default* view mapper Here's an example of a view mapper that emulates (somewhat) a Pylons "controller". The mapper is initialized with some keyword arguments. Its ``__call__`` method accepts the view object (which will be a class). It uses -the ``attr`` keyword argument it is passed to determine which attribute -should be used as an action method. The wrapper method it returns accepts -``(context, request)`` and returns the result of calling the action method -with keyword arguments implied by the :term:`matchdict` after popping the -``action`` out of it. This somewhat emulates the Pylons style of calling -action methods with routing parameters pulled out of the route matching dict -as keyword arguments. +the ``attr`` keyword argument it is passed to determine which attribute should +be used as an action method. The wrapper method it returns accepts ``(context, +request)`` and returns the result of calling the action method with keyword +arguments implied by the :term:`matchdict` after popping the ``action`` out of +it. This somewhat emulates the Pylons style of calling action methods with +routing parameters pulled out of the route matching dict as keyword arguments. .. code-block:: python :linenos: @@ -575,7 +873,7 @@ as keyword arguments. def wrapper(context, request): matchdict = request.matchdict.copy() matchdict.pop('action', None) - inst = view() + inst = view(request) meth = getattr(inst, attr) return meth(**matchdict) return wrapper @@ -590,10 +888,10 @@ A user might make use of these framework components like so: # user application - from webob import Response + from pyramid.response import Response from pyramid.config import Configurator import pyramid_handlers - from paste.httpserver import serve + from wsgiref.simple_server import make_server class MyController(BaseController): def index(self, id): @@ -604,14 +902,15 @@ A user might make use of these framework components like so: config.include(pyramid_handlers) config.add_handler('one', '/{id}', MyController, action='index') config.add_handler('two', '/{action}/{id}', MyController) - serve(config.make_wsgi_app()) + server.make_server('0.0.0.0', 8080, config.make_wsgi_app()) + server.serve_forever() The :meth:`pyramid.config.Configurator.set_view_mapper` method can be used to set a *default* view mapper (overriding the superdefault view mapper used by Pyramid itself). -A *single* view registration can use a view mapper by passing the mapper as -the ``mapper`` argument to :meth:`~pyramid.config.Configuration.add_view`. +A *single* view registration can use a view mapper by passing the mapper as the +``mapper`` argument to :meth:`~pyramid.config.Configurator.add_view`. .. index:: single: configuration decorator @@ -621,14 +920,14 @@ the ``mapper`` argument to :meth:`~pyramid.config.Configuration.add_view`. Registering Configuration Decorators ------------------------------------ -Decorators such as :class:`~pyramid.view.view_config` don't change the -behavior of the functions or classes they're decorating. Instead, when a -:term:`scan` is performed, a modified version of the function or class is -registered with :app:`Pyramid`. +Decorators such as :class:`~pyramid.view.view_config` don't change the behavior +of the functions or classes they're decorating. Instead when a :term:`scan` is +performed, a modified version of the function or class is registered with +:app:`Pyramid`. You may wish to have your own decorators that offer such behaviour. This is -possible by using the :term:`Venusian` package in the same way that it is -used by :app:`Pyramid`. +possible by using the :term:`Venusian` package in the same way that it is used +by :app:`Pyramid`. By way of example, let's suppose you want to write a decorator that registers the function it wraps with a :term:`Zope Component Architecture` "utility" @@ -638,8 +937,7 @@ available once your application's configuration is at least partially completed. A normal decorator would fail as it would be executed before the configuration had even begun. -However, using :term:`Venusian`, the decorator could be written as -follows: +However, using :term:`Venusian`, the decorator could be written as follows: .. code-block:: python :linenos: @@ -661,43 +959,772 @@ follows: venusian.attach(wrapped, self.register) return wrapped -This decorator could then be used to register functions throughout -your code: +This decorator could then be used to register functions throughout your code: .. code-block:: python :linenos: @registerFunction('/some/path') def my_function(): - do_stuff() + do_stuff() -However, the utility would only be looked up when a :term:`scan` was -performed, enabling you to set up the utility in advance: +However, the utility would only be looked up when a :term:`scan` was performed, +enabling you to set up the utility in advance: .. code-block:: python :linenos: - from paste.httpserver import serve + from zope.interface import implementer + + from wsgiref.simple_server import make_server from pyramid.config import Configurator from mypackage.interfaces import IMyUtility + @implementer(IMyUtility) class UtilityImplementation: - implements(IMyUtility) - def __init__(self): - self.registrations = {} + self.registrations = {} def register(self, path, callable_): - self.registrations[path] = callable_ + self.registrations[path] = callable_ if __name__ == '__main__': config = Configurator() config.registry.registerUtility(UtilityImplementation()) config.scan() app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() For full details, please read the `Venusian documentation <http://docs.repoze.org/venusian>`_. +.. _registering_tweens: + +Registering Tweens +------------------ + +.. versionadded:: 1.2 + Tweens + +A :term:`tween` (a contraction of the word "between") is a bit of code that +sits between the Pyramid router's main request handling function and the +upstream WSGI component that uses :app:`Pyramid` as its "app". This is a +feature that may be used by Pyramid framework extensions to provide, for +example, Pyramid-specific view timing support bookkeeping code that examines +exceptions before they are returned to the upstream WSGI application. Tweens +behave a bit like :term:`WSGI` :term:`middleware`, but they have the benefit of +running in a context in which they have access to the Pyramid :term:`request`, +:term:`response`, and :term:`application registry`, as well as the Pyramid +rendering machinery. + +Creating a Tween +~~~~~~~~~~~~~~~~ + +To create a tween, you must write a "tween factory". A tween factory must be a +globally importable callable which accepts two arguments: ``handler`` and +``registry``. ``handler`` will be either the main Pyramid request handling +function or another tween. ``registry`` will be the Pyramid :term:`application +registry` represented by this Configurator. A tween factory must return the +tween (a callable object) when it is called. + +A tween is called with a single argument, ``request``, which is the +:term:`request` created by Pyramid's router when it receives a WSGI request. A +tween should return a :term:`response`, usually the one generated by the +downstream Pyramid application. + +You can write the tween factory as a simple closure-returning function: + +.. code-block:: python + :linenos: + + def simple_tween_factory(handler, registry): + # one-time configuration code goes here + + def simple_tween(request): + # code to be executed for each request before + # the actual application code goes here + + response = handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + + return simple_tween + +Alternatively, the tween factory can be a class with the ``__call__`` magic +method: + +.. code-block:: python + :linenos: + + class simple_tween_factory(object): + def __init__(self, handler, registry): + self.handler = handler + self.registry = registry + + # one-time configuration code goes here + + def __call__(self, request): + # code to be executed for each request before + # the actual application code goes here + + response = self.handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + +You should avoid mutating any state on the tween instance. The tween is invoked +once per request and any shared mutable state needs to be carefully handled to +avoid any race conditions. + +The closure style performs slightly better and enables you to conditionally +omit the tween from the request processing pipeline (see the following timing +tween example), whereas the class style makes it easier to have shared mutable +state and allows subclassing. + +Here's a complete example of a tween that logs the time spent processing each +request: + +.. code-block:: python + :linenos: + + # in a module named myapp.tweens + + import time + from pyramid.settings import asbool + import logging + + log = logging.getLogger(__name__) + + def timing_tween_factory(handler, registry): + if asbool(registry.settings.get('do_timing')): + # if timing support is enabled, return a wrapper + def timing_tween(request): + start = time.time() + try: + response = handler(request) + finally: + end = time.time() + log.debug('The request took %s seconds' % + (end - start)) + return response + return timing_tween + # if timing support is not enabled, return the original + # handler + return handler + +In the above example, the tween factory defines a ``timing_tween`` tween and +returns it if ``asbool(registry.settings.get('do_timing'))`` is true. It +otherwise simply returns the handler which it was given. The +``registry.settings`` attribute is a handle to the deployment settings provided +by the user (usually in an ``.ini`` file). In this case, if the user has +defined a ``do_timing`` setting and that setting is ``True``, the user has said +they want to do timing, so the tween factory returns the timing tween; it +otherwise just returns the handler it has been provided, preventing any timing. + +The example timing tween simply records the start time, calls the downstream +handler, logs the number of seconds consumed by the downstream handler, and +returns the response. + +Registering an Implicit Tween Factory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you've created a tween factory, you can register it into the implicit +tween chain using the :meth:`pyramid.config.Configurator.add_tween` method +using its :term:`dotted Python name`. + +Here's an example of registering a tween factory as an "implicit" tween in a +Pyramid application: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + config = Configurator() + config.add_tween('myapp.tweens.timing_tween_factory') + +Note that you must use a :term:`dotted Python name` as the first argument to +:meth:`pyramid.config.Configurator.add_tween`; this must point at a tween +factory. You cannot pass the tween factory object itself to the method: it +must be :term:`dotted Python name` that points to a globally importable object. +In the above example, we assume that a ``timing_tween_factory`` tween factory +was defined in a module named ``myapp.tweens``, so the tween factory is +importable as ``myapp.tweens.timing_tween_factory``. + +When you use :meth:`pyramid.config.Configurator.add_tween`, you're instructing +the system to use your tween factory at startup time unless the user has +provided an explicit tween list in their configuration. This is what's meant +by an "implicit" tween. A user can always elect to supply an explicit tween +list, reordering or disincluding implicitly added tweens. See +:ref:`explicit_tween_ordering` for more information about explicit tween +ordering. + +If more than one call to :meth:`pyramid.config.Configurator.add_tween` is made +within a single application configuration, the tweens will be chained together +at application startup time. The *first* tween factory added via ``add_tween`` +will be called with the Pyramid exception view tween factory as its ``handler`` +argument, then the tween factory added directly after that one will be called +with the result of the first tween factory as its ``handler`` argument, and so +on, ad infinitum until all tween factories have been called. The Pyramid router +will use the outermost tween produced by this chain (the tween generated by the +very last tween factory added) as its request handler function. For example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + config = Configurator() + config.add_tween('myapp.tween_factory1') + config.add_tween('myapp.tween_factory2') + +The above example will generate an implicit tween chain that looks like this:: + + INGRESS (implicit) + myapp.tween_factory2 + myapp.tween_factory1 + pyramid.tweens.excview_tween_factory (implicit) + MAIN (implicit) + +Suggesting Implicit Tween Ordering +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, as described above, the ordering of the chain is controlled +entirely by the relative ordering of calls to +:meth:`pyramid.config.Configurator.add_tween`. However, the caller of +``add_tween`` can provide an optional hint that can influence the implicit +tween chain ordering by supplying ``under`` or ``over`` (or both) arguments to +:meth:`~pyramid.config.Configurator.add_tween`. These hints are only used when +an explicit tween ordering is not used. See :ref:`explicit_tween_ordering` for +a description of how to set an explicit tween ordering. + +Allowable values for ``under`` or ``over`` (or both) are: + +- ``None`` (the default), + +- a :term:`dotted Python name` to a tween factory: a string representing the + predicted dotted name of a tween factory added in a call to ``add_tween`` in + the same configuration session, + +- one of the constants :attr:`pyramid.tweens.MAIN`, + :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`, or + +- an iterable of any combination of the above. This allows the user to specify + fallbacks if the desired tween is not included, as well as compatibility + with multiple other tweens. + +Effectively, ``over`` means "closer to the request ingress than" and ``under`` +means "closer to the main Pyramid application than". You can think of an onion +with outer layers over the inner layers, the application being under all the +layers at the center. + +For example, the following call to +:meth:`~pyramid.config.Configurator.add_tween` will attempt to place the tween +factory represented by ``myapp.tween_factory`` directly "above" (in ``ptweens`` +order) the main Pyramid request handler. + +.. code-block:: python + :linenos: + + import pyramid.tweens + + config.add_tween('myapp.tween_factory', over=pyramid.tweens.MAIN) + +The above example will generate an implicit tween chain that looks like this:: + + INGRESS (implicit) + pyramid.tweens.excview_tween_factory (implicit) + myapp.tween_factory + MAIN (implicit) + +Likewise, calling the following call to +:meth:`~pyramid.config.Configurator.add_tween` will attempt to place this tween +factory "above" the main handler but "below" a separately added tween factory: + +.. code-block:: python + :linenos: + + import pyramid.tweens + + config.add_tween('myapp.tween_factory1', + over=pyramid.tweens.MAIN) + config.add_tween('myapp.tween_factory2', + over=pyramid.tweens.MAIN, + under='myapp.tween_factory1') + +The above example will generate an implicit tween chain that looks like this:: + + INGRESS (implicit) + pyramid.tweens.excview_tween_factory (implicit) + myapp.tween_factory1 + myapp.tween_factory2 + MAIN (implicit) + +Specifying neither ``over`` nor ``under`` is equivalent to specifying +``under=INGRESS``. + +If all options for ``under`` (or ``over``) cannot be found in the current +configuration, it is an error. If some options are specified purely for +compatibilty with other tweens, just add a fallback of ``MAIN`` or ``INGRESS``. +For example, ``under=('someothertween', 'someothertween2', INGRESS)``. This +constraint will require the tween to be located under the ``someothertween`` +tween, the ``someothertween2`` tween, and ``INGRESS``. If any of these is not +in the current configuration, this constraint will only organize itself based +on the tweens that are present. + +.. _explicit_tween_ordering: + +Explicit Tween Ordering +~~~~~~~~~~~~~~~~~~~~~~~ + +Implicit tween ordering is obviously only best-effort. Pyramid will attempt to +provide an implicit order of tweens as best it can using hints provided by +calls to :meth:`~pyramid.config.Configurator.add_tween`. But because it's only +best-effort, if very precise tween ordering is required, the only surefire way +to get it is to use an explicit tween order. The deploying user can override +the implicit tween inclusion and ordering implied by calls to +:meth:`~pyramid.config.Configurator.add_tween` entirely by using the +``pyramid.tweens`` settings value. When used, this settings value must be a +list of Python dotted names which will override the ordering (and inclusion) of +tween factories in the implicit tween chain. For example: + +.. code-block:: ini + :linenos: + + [app:main] + use = egg:MyApp + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.debug_routematch = false + pyramid.debug_templates = true + pyramid.tweens = myapp.my_cool_tween_factory + pyramid.tweens.excview_tween_factory + +In the above configuration, calls made during configuration to +:meth:`pyramid.config.Configurator.add_tween` are ignored, and the user is +telling the system to use the tween factories he has listed in the +``pyramid.tweens`` configuration setting (each is a :term:`dotted Python name` +which points to a tween factory) instead of any tween factories added via +:meth:`pyramid.config.Configurator.add_tween`. The *first* tween factory in +the ``pyramid.tweens`` list will be used as the producer of the effective +:app:`Pyramid` request handling function; it will wrap the tween factory +declared directly "below" it, ad infinitum. The "main" Pyramid request handler +is implicit, and always "at the bottom". + +.. note:: + + Pyramid's own :term:`exception view` handling logic is implemented as a + tween factory function: :func:`pyramid.tweens.excview_tween_factory`. If + Pyramid exception view handling is desired, and tween factories are + specified via the ``pyramid.tweens`` configuration setting, the + :func:`pyramid.tweens.excview_tween_factory` function must be added to the + ``pyramid.tweens`` configuration setting list explicitly. If it is not + present, Pyramid will not perform exception view handling. + +Tween Conflicts and Ordering Cycles +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid will prevent the same tween factory from being added to the tween chain +more than once using configuration conflict detection. If you wish to add the +same tween factory more than once in a configuration, you should either: (a) +use a tween factory that is a separate globally importable instance object from +the factory that it conflicts with; (b) use a function or class as a tween +factory with the same logic as the other tween factory it conflicts with, but +with a different ``__name__`` attribute; or (c) call +:meth:`pyramid.config.Configurator.commit` between calls to +:meth:`pyramid.config.Configurator.add_tween`. + +If a cycle is detected in implicit tween ordering when ``over`` and ``under`` +are used in any call to ``add_tween``, an exception will be raised at startup +time. + +Displaying Tween Ordering +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``ptweens`` command-line utility can be used to report the current implict +and explicit tween chains used by an application. See +:ref:`displaying_tweens`. + +.. _registering_thirdparty_predicates: + +Adding a Third Party View, Route, or Subscriber Predicate +--------------------------------------------------------- + +.. versionadded:: 1.4 + +.. _view_and_route_predicates: + +View and Route Predicates +~~~~~~~~~~~~~~~~~~~~~~~~~ + +View and route predicates used during configuration allow you to narrow the set +of circumstances under which a view or route will match. For example, the +``request_method`` view predicate can be used to ensure a view callable is only +invoked when the request's method is ``POST``: + +.. code-block:: python + + @view_config(request_method='POST') + def someview(request): + ... + +Likewise, a similar predicate can be used as a *route* predicate: + +.. code-block:: python + + config.add_route('name', '/foo', request_method='POST') + +Many other built-in predicates exists (``request_param``, and others). You can +add third-party predicates to the list of available predicates by using one of +:meth:`pyramid.config.Configurator.add_view_predicate` or +:meth:`pyramid.config.Configurator.add_route_predicate`. The former adds a +view predicate, the latter a route predicate. + +When using one of those APIs, you pass a *name* and a *factory* to add a +predicate during Pyramid's configuration stage. For example: + +.. code-block:: python + + config.add_view_predicate('content_type', ContentTypePredicate) + +The above example adds a new predicate named ``content_type`` to the list of +available predicates for views. This will allow the following view +configuration statement to work: + +.. code-block:: python + :linenos: + + @view_config(content_type='File') + def aview(request): ... + +The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, +the name, is a string representing the name that is expected to be passed to +``view_config`` (or its imperative analogue ``add_view``). + +The second argument is a view or route predicate factory, or a :term:`dotted +Python name` which refers to a view or route predicate factory. A view or +route predicate factory is most often a class with a constructor +(``__init__``), a ``text`` method, a ``phash`` method, and a ``__call__`` +method. For example: + +.. code-block:: python + :linenos: + + class ContentTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'content_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return getattr(context, 'content_type', None) == self.val + +The constructor of a predicate factory takes two arguments: ``val`` and +``config``. The ``val`` argument will be the argument passed to +``view_config`` (or ``add_view``). In the example above, it will be the string +``File``. The second argument, ``config``, will be the Configurator instance +at the time of configuration. + +The ``text`` method must return a string. It should be useful to describe the +behavior of the predicate in error messages. + +The ``phash`` method must return a string or a sequence of strings. It's most +often the same as ``text``, as long as ``text`` uniquely describes the +predicate's name and the value passed to the constructor. If ``text`` is more +general, or doesn't describe things that way, ``phash`` should return a string +with the name and the value serialized. The result of ``phash`` is not seen in +output anywhere, it just informs the uniqueness constraints for view +configuration. + +The ``__call__`` method of a predicate factory must accept a resource +(``context``) and a request, and must return ``True`` or ``False``. It is the +"meat" of the predicate. + +You can use the same predicate factory as both a view predicate and as a route +predicate, but you'll need to call ``add_view_predicate`` and +``add_route_predicate`` separately with the same factory. + +.. _subscriber_predicates: + +Subscriber Predicates +~~~~~~~~~~~~~~~~~~~~~ + +Subscriber predicates work almost exactly like view and route predicates. They +narrow the set of circumstances in which a subscriber will be called. There are +several minor differences between a subscriber predicate and a view or route +predicate: + +- There are no default subscriber predicates. You must register one to use + one. + +- The ``__call__`` method of a subscriber predicate accepts a single ``event`` + object instead of a ``context`` and a ``request``. + +- Not every subscriber predicate can be used with every event type. Some + subscriber predicates will assume a certain event type. + +Here's an example of a subscriber predicate that can be used in conjunction +with a subscriber that subscribes to the :class:`pyramid.events.NewRequest` +event type. + +.. code-block:: python + :linenos: + + class RequestPathStartsWith(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'path_startswith = %s' % (self.val,) + + phash = text + + def __call__(self, event): + return event.request.path.startswith(self.val) + +Once you've created a subscriber predicate, it may registered via +:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example: + +.. code-block:: python + + config.add_subscriber_predicate( + 'request_path_startswith', RequestPathStartsWith) + +Once a subscriber predicate is registered, you can use it in a call to +:meth:`pyramid.config.Configurator.add_subscriber` or to +:class:`pyramid.events.subscriber`. Here's an example of using the previously +registered ``request_path_startswith`` predicate in a call to +:meth:`~pyramid.config.Configurator.add_subscriber`: + +.. code-block:: python + :linenos: + + # define a subscriber in your code + + def yosubscriber(event): + event.request.yo = 'YO!' + + # and at configuration time + + config.add_subscriber(yosubscriber, NewRequest, + request_path_startswith='/add_yo') + +Here's the same subscriber/predicate/event-type combination used via +:class:`~pyramid.events.subscriber`. + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + + @subscriber(NewRequest, request_path_startswith='/add_yo') + def yosubscriber(event): + event.request.yo = 'YO!' + +In either of the above configurations, the ``yosubscriber`` callable will only +be called if the request path starts with ``/add_yo``. Otherwise the event +subscriber will not be called. + +Note that the ``request_path_startswith`` subscriber you defined can be used +with events that have a ``request`` attribute, but not ones that do not. So, +for example, the predicate can be used with subscribers registered for +:class:`pyramid.events.NewRequest` and :class:`pyramid.events.ContextFound` +events, but it cannot be used with subscribers registered for +:class:`pyramid.events.ApplicationCreated` because the latter type of event has +no ``request`` attribute. The point being, unlike route and view predicates, +not every type of subscriber predicate will necessarily be applicable for use +in every subscriber registration. It is not the responsibility of the +predicate author to make every predicate make sense for every event type; it is +the responsibility of the predicate consumer to use predicates that make sense +for a particular event type registration. + + +.. index:: + single: view derivers + +.. _view_derivers: + +View Derivers +------------- + +.. versionadded:: 1.7 + +Every URL processed by :app:`Pyramid` is matched against a custom view +pipeline. See :ref:`router_chapter` for how this works. The view pipeline +itself is built from the user-supplied :term:`view callable`, which is then +composed with :term:`view derivers <view deriver>`. A view deriver is a +composable element of the view pipeline which is used to wrap a view with +added functionality. View derivers are very similar to the ``decorator`` +argument to :meth:`pyramid.config.Configurator.add_view`, except that they have +the option to execute for every view in the application. + +It is helpful to think of a :term:`view deriver` as middleware for views. +Unlike tweens or WSGI middleware which are scoped to the application itself, +a view deriver is invoked once per view in the application, and can use +configuration options from the view to customize its behavior. + +Built-in View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +There are several built-in view derivers that :app:`Pyramid` will automatically +apply to any view. Below they are defined in order from furthest to closest to +the user-defined :term:`view callable`: + +``secured_view`` + + Enforce the ``permission`` defined on the view. This element is a no-op if no + permission is defined. Note there will always be a permission defined if a + default permission was assigned via + :meth:`pyramid.config.Configurator.set_default_permission`. + + This element will also output useful debugging information when + ``pyramid.debug_authorization`` is enabled. + +``csrf_view`` + + Used to check the CSRF token provided in the request. This element is a + no-op if both the ``require_csrf`` view option and the + ``pyramid.require_default_csrf`` setting are disabled. + +``owrapped_view`` + + Invokes the wrapped view defined by the ``wrapper`` option. + +``http_cached_view`` + + Applies cache control headers to the response defined by the ``http_cache`` + option. This element is a no-op if the ``pyramid.prevent_http_cache`` setting + is enabled or the ``http_cache`` option is ``None``. + +``decorated_view`` + + Wraps the view with the decorators from the ``decorator`` option. + +``rendered_view`` + + Adapts the result of the :term:`view callable` into a :term:`response` + object. Below this point the result may be any Python object. + +``mapped_view`` + + Applies the :term:`view mapper` defined by the ``mapper`` option or the + application's default view mapper to the :term:`view callable`. This + is always the closest deriver to the user-defined view and standardizes the + view pipeline interface to accept ``(context, request)`` from all previous + view derivers. + +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. + +Custom View Derivers +~~~~~~~~~~~~~~~~~~~~ + +It is possible to define custom view derivers which will affect all views in an +application. There are many uses for this, but most will likely be centered +around monitoring and security. In order to register a custom :term:`view +deriver`, you should create a callable that conforms to the +:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with +your application using :meth:`pyramid.config.Configurator.add_view_deriver`. +For example, below is a callable that can provide timing information for the +view pipeline: + +.. code-block:: python + :linenos: + + import time + + def timing_view(view, info): + if info.options.get('timed'): + def wrapper_view(context, request): + start = time.time() + response = view(context, request) + end = time.time() + response.headers['X-View-Performance'] = '%.3f' % (end - start,) + return response + return wrapper_view + return view + + timing_view.options = ('timed',) + + config.add_view_deriver(timing_view) + +The setting of ``timed`` on the timing_view signifies to Pyramid that ``timed`` +is a valid ``view_config`` keyword argument now. The ``timing_view`` custom +view deriver as registered above will only be active for any view defined with +a ``timed=True`` value passed as one of its ``view_config`` keywords. + +For example, this view configuration will *not* be a timed view: + +.. code-block:: python + :linenos: + + @view_config(route_name='home') + def home(request): + return Response('Home') + +But this view *will* have timing information added to the response headers: + +.. code-block:: python + :linenos: + + @view_config(route_name='home', timed=True) + def home(request): + return Response('Home') + +View derivers are unique in that they have access to most of the options +passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what +to do, and they have a chance to affect every view in the application. + +Ordering View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +By default, every new view deriver is added between the ``decorated_view`` and +``rendered_view`` built-in derivers. It is possible to customize this ordering +using the ``over`` and ``under`` options. Each option can use the names of +other view derivers in order to specify an ordering. There should rarely be a +reason to worry about the ordering of the derivers except when the deriver +depends on other operations in the view pipeline. + +Both ``over`` and ``under`` may also be iterables of constraints. For either +option, if one or more constraints was defined, at least one must be satisfied, +else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may +be used to define fallback constraints if another deriver is missing. + +Two sentinel values exist, :attr:`pyramid.viewderivers.INGRESS` and +:attr:`pyramid.viewderivers.VIEW`, which may be used when specifying +constraints at the edges of the view pipeline. For example, to add a deriver +at the start of the pipeline you may use ``under=INGRESS``. + +It is not possible to add a view deriver under the ``mapped_view`` as the +:term:`view mapper` is intimately tied to the signature of the user-defined +:term:`view callable`. If you simply need to know what the original view +callable was, it can be found as ``info.original_view`` on the provided +:class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view +deriver. + +.. warning:: + + The default constraints for any view deriver are ``over='rendered_view'`` + and ``under='decorated_view'``. When escaping these constraints you must + take care to avoid cyclic dependencies between derivers. For example, if + you want to add a new view deriver before ``secured_view`` then + simply specifying ``over='secured_view'`` is not enough, because the + default is also under ``decorated view`` there will be an unsatisfiable + cycle. You must specify a valid ``under`` constraint as well, such as + ``under=INGRESS`` to fall between INGRESS and ``secured_view`` at the + beginning of the view pipeline. diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst index 97adaeafd..ff26d52ec 100644 --- a/docs/narr/hybrid.rst +++ b/docs/narr/hybrid.rst @@ -12,28 +12,26 @@ dispatch. However, to solve a limited set of problems, it's useful to use .. warning:: Reasoning about the behavior of a "hybrid" URL dispatch + traversal - application can be challenging. To successfully reason about using - URL dispatch and traversal together, you need to understand URL - pattern matching, root factories, and the :term:`traversal` - algorithm, and the potential interactions between them. Therefore, - we don't recommend creating an application that relies on hybrid - behavior unless you must. + application can be challenging. To successfully reason about using URL + dispatch and traversal together, you need to understand URL pattern + matching, root factories, and the :term:`traversal` algorithm, and the + potential interactions between them. Therefore, we don't recommend creating + an application that relies on hybrid behavior unless you must. A Review of Non-Hybrid Applications ----------------------------------- -When used according to the tutorials in its documentation -:app:`Pyramid` is a "dual-mode" framework: the tutorials explain -how to create an application in terms of using either :term:`url -dispatch` *or* :term:`traversal`. This chapter details how you might -combine these two dispatch mechanisms, but we'll review how they work -in isolation before trying to combine them. +When used according to the tutorials in its documentation, :app:`Pyramid` is a +"dual-mode" framework: the tutorials explain how to create an application in +terms of using either :term:`URL dispatch` *or* :term:`traversal`. This +chapter details how you might combine these two dispatch mechanisms, but we'll +review how they work in isolation before trying to combine them. URL Dispatch Only ~~~~~~~~~~~~~~~~~ -An application that uses :term:`url dispatch` exclusively to map URLs to code -will often have statements like this within application startup +An application that uses :term:`URL dispatch` exclusively to map URLs to code +will often have statements like this within its application startup configuration: .. code-block:: python @@ -48,11 +46,11 @@ configuration: config.add_view('myproject.views.bazbuz', route_name='bazbuz') Each :term:`route` corresponds to one or more view callables. Each view -callable is associated with a route by passing a ``route_name`` parameter -that matches its name during a call to -:meth:`~pyramid.config.Configurator.add_view`. When a route is matched -during a request, :term:`view lookup` is used to match the request to its -associated view callable. The presence of calls to +callable is associated with a route by passing a ``route_name`` parameter that +matches its name during a call to +:meth:`~pyramid.config.Configurator.add_view`. When a route is matched during +a request, :term:`view lookup` is used to match the request to its associated +view callable. The presence of calls to :meth:`~pyramid.config.Configurator.add_route` signify that an application is using URL dispatch. @@ -63,7 +61,7 @@ An application that uses only traversal will have view configuration declarations that look like this: .. code-block:: python - :linenos: + :linenos: # config is an instance of pyramid.config.Configurator @@ -72,156 +70,151 @@ declarations that look like this: When the above configuration is applied to an application, the ``mypackage.views.foobar`` view callable above will be called when the URL -``/foobar`` is visited. Likewise, the view ``mypackage.views.bazbuz`` will -be called when the URL ``/bazbuz`` is visited. +``/foobar`` is visited. Likewise, the view ``mypackage.views.bazbuz`` will be +called when the URL ``/bazbuz`` is visited. Typically, an application that uses traversal exclusively won't perform any -calls to :meth:`pyramid.config.Configurator.add_route` in its startup -code. +calls to :meth:`pyramid.config.Configurator.add_route` in its startup code. + +.. index:: + single: hybrid applications Hybrid Applications ------------------- -Either traversal or url dispatch alone can be used to create a -:app:`Pyramid` application. However, it is also possible to -combine the concepts of traversal and url dispatch when building an -application: the result is a hybrid application. In a hybrid -application, traversal is performed *after* a particular route has -matched. - -A hybrid application is a lot more like a "pure" traversal-based -application than it is like a "pure" URL-dispatch based application. -But unlike in a "pure" traversal-based application, in a hybrid -application, :term:`traversal` is performed during a request after a -route has already matched. This means that the URL pattern that -represents the ``pattern`` argument of a route must match the -``PATH_INFO`` of a request, and after the route pattern has matched, -most of the "normal" rules of traversal with respect to :term:`resource -location` and :term:`view lookup` apply. +Either traversal or URL dispatch alone can be used to create a :app:`Pyramid` +application. However, it is also possible to combine the concepts of traversal +and URL dispatch when building an application, the result of which is a hybrid +application. In a hybrid application, traversal is performed *after* a +particular route has matched. + +A hybrid application is a lot more like a "pure" traversal-based application +than it is like a "pure" URL-dispatch based application. But unlike in a "pure" +traversal-based application, in a hybrid application :term:`traversal` is +performed during a request after a route has already matched. This means that +the URL pattern that represents the ``pattern`` argument of a route must match +the ``PATH_INFO`` of a request, and after the route pattern has matched, most +of the "normal" rules of traversal with respect to :term:`resource location` +and :term:`view lookup` apply. There are only four real differences between a purely traversal-based application and a hybrid application: -- In a purely traversal based application, no routes are defined; in a - hybrid application, at least one route will be defined. +- In a purely traversal-based application, no routes are defined. In a hybrid + application, at least one route will be defined. -- In a purely traversal based application, the root object used is - global, implied by the :term:`root factory` provided at startup - time; in a hybrid application, the :term:`root` object at which - traversal begins may be varied on a per-route basis. +- In a purely traversal-based application, the root object used is global, + implied by the :term:`root factory` provided at startup time. In a hybrid + application, the :term:`root` object at which traversal begins may be varied + on a per-route basis. -- In a purely traversal-based application, the ``PATH_INFO`` of the - underlying :term:`WSGI` environment is used wholesale as a traversal - path; in a hybrid application, the traversal path is not the entire - ``PATH_INFO`` string, but a portion of the URL determined by a - matching pattern in the matched route configuration's pattern. +- In a purely traversal-based application, the ``PATH_INFO`` of the underlying + :term:`WSGI` environment is used wholesale as a traversal path. In a hybrid + application, the traversal path is not the entire ``PATH_INFO`` string, but a + portion of the URL determined by a matching pattern in the matched route + configuration's pattern. -- In a purely traversal based application, view configurations which - do not mention a ``route_name`` argument are considered during - :term:`view lookup`; in a hybrid application, when a route is - matched, only view configurations which mention that route's name as - a ``route_name`` are considered during :term:`view lookup`. +- In a purely traversal-based application, view configurations which do not + mention a ``route_name`` argument are considered during :term:`view lookup`. + In a hybrid application, when a route is matched, only view configurations + which mention that route's name as a ``route_name`` are considered during + :term:`view lookup`. -More generally, a hybrid application *is* a traversal-based -application except: +More generally, a hybrid application *is* a traversal-based application except: -- the traversal *root* is chosen based on the route configuration of - the route that matched instead of from the ``root_factory`` supplied - during application startup configuration. +- the traversal *root* is chosen based on the route configuration of the route + that matched, instead of from the ``root_factory`` supplied during + application startup configuration. -- the traversal *path* is chosen based on the route configuration of - the route that matched rather than from the ``PATH_INFO`` of a - request. +- the traversal *path* is chosen based on the route configuration of the route + that matched, rather than from the ``PATH_INFO`` of a request. -- the set of views that may be chosen during :term:`view lookup` when - a route matches are limited to those which specifically name a - ``route_name`` in their configuration that is the same as the - matched route's ``name``. +- the set of views that may be chosen during :term:`view lookup` when a route + matches are limited to those which specifically name a ``route_name`` in + their configuration that is the same as the matched route's ``name``. -To create a hybrid mode application, use a :term:`route configuration` -that implies a particular :term:`root factory` and which also includes -a ``pattern`` argument that contains a special dynamic part: either -``*traverse`` or ``*subpath``. +To create a hybrid mode application, use a :term:`route configuration` that +implies a particular :term:`root factory` and which also includes a ``pattern`` +argument that contains a special dynamic part: either ``*traverse`` or +``*subpath``. The Root Object for a Route Match ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A hybrid application implies that traversal is performed during a -request after a route has matched. Traversal, by definition, must -always begin at a root object. Therefore it's important to know -*which* root object will be traversed after a route has matched. +A hybrid application implies that traversal is performed during a request after +a route has matched. Traversal, by definition, must always begin at a root +object. Therefore it's important to know *which* root object will be traversed +after a route has matched. -Figuring out which :term:`root` object results from a particular route -match is straightforward. When a route is matched: +Figuring out which :term:`root` object results from a particular route match is +straightforward. When a route is matched: -- If the route's configuration has a ``factory`` argument which - points to a :term:`root factory` callable, that callable will be - called to generate a :term:`root` object. +- If the route's configuration has a ``factory`` argument which points to a + :term:`root factory` callable, that callable will be called to generate a + :term:`root` object. -- If the route's configuration does not have a ``factory`` - argument, the *global* :term:`root factory` will be called to - generate a :term:`root` object. The global root factory is the - callable implied by the ``root_factory`` argument passed to the - :class:`~pyramid.config.Configurator` at application - startup time. +- If the route's configuration does not have a ``factory`` argument, the + *global* :term:`root factory` will be called to generate a :term:`root` + object. The global root factory is the callable implied by the + ``root_factory`` argument passed to the :class:`~pyramid.config.Configurator` + at application startup time. - If a ``root_factory`` argument is not provided to the - :class:`~pyramid.config.Configurator` at startup time, a - *default* root factory is used. The default root factory is used to - generate a root object. + :class:`~pyramid.config.Configurator` at startup time, a *default* root + factory is used. The default root factory is used to generate a root object. .. note:: Root factories related to a route were explained previously within - :ref:`route_factories`. Both the global root factory and default - root factory were explained previously within - :ref:`the_resource_tree`. + :ref:`route_factories`. Both the global root factory and default root + factory were explained previously within :ref:`the_resource_tree`. + +.. index:: + pair: hybrid applications; *traverse route pattern .. _using_traverse_in_a_route_pattern: -Using ``*traverse`` In a Route Pattern +Using ``*traverse`` in a Route Pattern ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A hybrid application most often implies the inclusion of a route -configuration that contains the special token ``*traverse`` at the end -of a route's pattern: +A hybrid application most often implies the inclusion of a route configuration +that contains the special token ``*traverse`` at the end of a route's pattern: .. code-block:: python :linenos: config.add_route('home', '{foo}/{bar}/*traverse') -A ``*traverse`` token at the end of the pattern in a route's -configuration implies a "remainder" *capture* value. When it is used, -it will match the remainder of the path segments of the URL. This -remainder becomes the path used to perform traversal. +A ``*traverse`` token at the end of the pattern in a route's configuration +implies a "remainder" *capture* value. When it is used, it will match the +remainder of the path segments of the URL. This remainder becomes the path +used to perform traversal. .. note:: - The ``*remainder`` route pattern syntax is explained in more - detail within :ref:`route_pattern_syntax`. + The ``*remainder`` route pattern syntax is explained in more detail within + :ref:`route_pattern_syntax`. A hybrid mode application relies more heavily on :term:`traversal` to do :term:`resource location` and :term:`view lookup` than most examples indicate within :ref:`urldispatch_chapter`. -Because the pattern of the above route ends with ``*traverse``, when this -route configuration is matched during a request, :app:`Pyramid` will attempt -to use :term:`traversal` against the :term:`root` object implied by the -:term:`root factory` that is implied by the route's configuration. Since no +Because the pattern of the above route ends with ``*traverse``, when this route +configuration is matched during a request, :app:`Pyramid` will attempt to use +:term:`traversal` against the :term:`root` object implied by the :term:`root +factory` that is implied by the route's configuration. Since no ``root_factory`` argument is explicitly specified for this route, this will -either be the *global* root factory for the application, or the *default* -root factory. Once :term:`traversal` has found a :term:`context` resource, +either be the *global* root factory for the application, or the *default* root +factory. Once :term:`traversal` has found a :term:`context` resource, :term:`view lookup` will be invoked in almost exactly the same way it would have been invoked in a "pure" traversal-based application. -Let's assume there is no *global* :term:`root factory` configured in -this application. The *default* :term:`root factory` cannot be traversed: -it has no useful ``__getitem__`` method. So we'll need to associate -this route configuration with a custom root factory in order to -create a useful hybrid application. To that end, let's imagine that -we've created a root factory that looks like so in a module named -``routes.py``: +Let's assume there is no *global* :term:`root factory` configured in this +application. The *default* :term:`root factory` cannot be traversed; it has no +useful ``__getitem__`` method. So we'll need to associate this route +configuration with a custom root factory in order to create a useful hybrid +application. To that end, let's imagine that we've created a root factory that +looks like so in a module named ``routes.py``: .. code-block:: python :linenos: @@ -240,7 +233,7 @@ we've created a root factory that looks like so in a module named def root_factory(request): return root -Above, we've defined a (bogus) resource tree that can be traversed, and a +Above we've defined a (bogus) resource tree that can be traversed, and a ``root_factory`` function that can be used as part of a particular route configuration statement: @@ -250,8 +243,8 @@ configuration statement: config.add_route('home', '{foo}/{bar}/*traverse', factory='mypackage.routes.root_factory') -The ``factory`` above points at the function we've defined. It will return -an instance of the ``Resource`` class as a root object whenever this route is +The ``factory`` above points at the function we've defined. It will return an +instance of the ``Resource`` class as a root object whenever this route is matched. Instances of the ``Resource`` class can be used for tree traversal because they have a ``__getitem__`` method that does something nominally useful. Since traversal uses ``__getitem__`` to walk the resources of a @@ -260,39 +253,37 @@ statement is a reasonable thing to do. .. note:: - We could have also used our ``root_factory`` function as the - ``root_factory`` argument of the - :class:`~pyramid.config.Configurator` constructor, instead - of associating it with a particular route inside the route's - configuration. Every hybrid route configuration that is matched but - which does *not* name a ``factory`` attribute will use the use - global ``root_factory`` function to generate a root object. + We could have also used our ``root_factory`` function as the ``root_factory`` + argument of the :class:`~pyramid.config.Configurator` constructor, instead of + associating it with a particular route inside the route's configuration. + Every hybrid route configuration that is matched, but which does *not* name a + ``factory`` attribute, will use the global ``root_factory`` function to + generate a root object. -When the route configuration named ``home`` above is matched during a -request, the matchdict generated will be based on its pattern: +When the route configuration named ``home`` above is matched during a request, +the matchdict generated will be based on its pattern: ``{foo}/{bar}/*traverse``. The "capture value" implied by the ``*traverse`` element in the pattern will be used to traverse the resource tree in order to find a context resource, starting from the root object returned from the root factory. In the above example, the :term:`root` object found will be the instance named ``root`` in ``routes.py``. -If the URL that matched a route with the pattern ``{foo}/{bar}/*traverse``, -is ``http://example.com/one/two/a/b/c``, the traversal path used -against the root object will be ``a/b/c``. As a result, -:app:`Pyramid` will attempt to traverse through the edges ``'a'``, -``'b'``, and ``'c'``, beginning at the root object. +If the URL that matched a route with the pattern ``{foo}/{bar}/*traverse`` is +``http://example.com/one/two/a/b/c``, the traversal path used against the root +object will be ``a/b/c``. As a result, :app:`Pyramid` will attempt to traverse +through the edges ``'a'``, ``'b'``, and ``'c'``, beginning at the root object. -In our above example, this particular set of traversal steps will mean that -the :term:`context` resource of the view would be the ``Resource`` object -we've named ``'c'`` in our bogus resource tree and the :term:`view name` -resulting from traversal will be the empty string; if you need a refresher -about why this outcome is presumed, see :ref:`traversal_algorithm`. +In our above example, this particular set of traversal steps will mean that the +:term:`context` resource of the view would be the ``Resource`` object we've +named ``'c'`` in our bogus resource tree, and the :term:`view name` resulting +from traversal will be the empty string. If you need a refresher about why +this outcome is presumed, see :ref:`traversal_algorithm`. -At this point, a suitable view callable will be found and invoked -using :term:`view lookup` as described in :ref:`view_configuration`, -but with a caveat: in order for view lookup to work, we need to define -a view configuration that will match when :term:`view lookup` is -invoked after a route matches: +At this point, a suitable view callable will be found and invoked using +:term:`view lookup` as described in :ref:`view_configuration`, but with a +caveat: in order for view lookup to work, we need to define a view +configuration that will match when :term:`view lookup` is invoked after a route +matches: .. code-block:: python :linenos: @@ -301,28 +292,28 @@ invoked after a route matches: factory='mypackage.routes.root_factory') config.add_view('mypackage.views.myview', route_name='home') -Note that the above call to -:meth:`~pyramid.config.Configurator.add_view` includes a ``route_name`` -argument. View configurations that include a ``route_name`` argument are -meant to associate a particular view declaration with a route, using the -route's name, in order to indicate that the view should *only be invoked when -the route matches*. +Note that the above call to :meth:`~pyramid.config.Configurator.add_view` +includes a ``route_name`` argument. View configurations that include a +``route_name`` argument are meant to associate a particular view declaration +with a route, using the route's name, in order to indicate that the view should +*only be invoked when the route matches*. Calls to :meth:`~pyramid.config.Configurator.add_view` may pass a ``route_name`` attribute, which refers to the value of an existing route's -``name`` argument. In the above example, the route name is ``home``, -referring to the name of the route defined above it. +``name`` argument. In the above example, the route name is ``home``, referring +to the name of the route defined above it. -The above ``mypackage.views.myview`` view callable will be invoked when: +The above ``mypackage.views.myview`` view callable will be invoked when the +following conditions are met: -- the route named "home" is matched +- The route named "home" is matched. -- the :term:`view name` resulting from traversal is the empty string. +- The :term:`view name` resulting from traversal is the empty string. -- the :term:`context` resource is any object. +- The :term:`context` resource is any object. -It is also possible to declare alternate views that may be invoked -when a hybrid route is matched: +It is also possible to declare alternative views that may be invoked when a +hybrid route is matched: .. code-block:: python :linenos: @@ -334,37 +325,37 @@ when a hybrid route is matched: name='another') The ``add_view`` call for ``mypackage.views.another_view`` above names a -different view and, more importantly, a different :term:`view name`. The -above ``mypackage.views.another_view`` view will be invoked when: +different view and, more importantly, a different :term:`view name`. The above +``mypackage.views.another_view`` view will be invoked when the following +conditions are met: -- the route named "home" is matched +- The route named "home" is matched. -- the :term:`view name` resulting from traversal is ``another``. +- The :term:`view name` resulting from traversal is ``another``. -- the :term:`context` resource is any object. +- The :term:`context` resource is any object. For instance, if the URL ``http://example.com/one/two/a/another`` is provided to an application that uses the previously mentioned resource tree, the -``mypackage.views.another`` view callable will be called instead of the -``mypackage.views.myview`` view callable because the :term:`view name` will -be ``another`` instead of the empty string. +``mypackage.views.another_view`` view callable will be called instead of the +``mypackage.views.myview`` view callable because the :term:`view name` will be +``another`` instead of the empty string. More complicated matching can be composed. All arguments to *route* -configuration statements and *view* configuration statements are -supported in hybrid applications (such as :term:`predicate` -arguments). +configuration statements and *view* configuration statements are supported in +hybrid applications (such as :term:`predicate` arguments). -Using the ``traverse`` Argument In a Route Definition +Using the ``traverse`` Argument in a Route Definition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Rather than using the ``*traverse`` remainder marker in a pattern, you -can use the ``traverse`` argument to the -:meth:`~pyramid.config.Configurator.add_route` method. +Rather than using the ``*traverse`` remainder marker in a pattern, you can use +the ``traverse`` argument to the :meth:`~pyramid.config.Configurator.add_route` +method. -When you use the ``*traverse`` remainder marker, the traversal path is -limited to being the remainder segments of a request URL when a route -matches. However, when you use the ``traverse`` argument or -attribute, you have more control over how to compose a traversal path. +When you use the ``*traverse`` remainder marker, the traversal path is limited +to being the remainder segments of a request URL when a route matches. +However, when you use the ``traverse`` argument or attribute, you have more +control over how to compose a traversal path. Here's a use of the ``traverse`` pattern in a call to :meth:`~pyramid.config.Configurator.add_route`: @@ -375,42 +366,41 @@ Here's a use of the ``traverse`` pattern in a call to config.add_route('abc', '/articles/{article}/edit', traverse='/{article}') -The syntax of the ``traverse`` argument is the same as it is for -``pattern``. +The syntax of the ``traverse`` argument is the same as it is for ``pattern``. -If, as above, the ``pattern`` provided is ``/articles/{article}/edit``, -and the ``traverse`` argument provided is ``/{article}``, when a -request comes in that causes the route to match in such a way that the -``article`` match value is ``1`` (when the request URI is -``/articles/1/edit``), the traversal path will be generated as ``/1``. -This means that the root object's ``__getitem__`` will be called with -the name ``1`` during the traversal phase. If the ``1`` object -exists, it will become the :term:`context` of the request. -The :ref:`traversal_chapter` chapter has more information about traversal. +If, as above, the ``pattern`` provided is ``/articles/{article}/edit``, and the +``traverse`` argument provided is ``/{article}``, when a request comes in that +causes the route to match in such a way that the ``article`` match value is +``1`` (when the request URI is ``/articles/1/edit``), the traversal path will +be generated as ``/1``. This means that the root object's ``__getitem__`` will +be called with the name ``1`` during the traversal phase. If the ``1`` object +exists, it will become the :term:`context` of the request. The +:ref:`traversal_chapter` chapter has more information about traversal. -If the traversal path contains segment marker names which are not -present in the pattern argument, a runtime error will occur. The -``traverse`` pattern should not contain segment markers that do not -exist in the ``path``. +If the traversal path contains segment marker names which are not present in +the pattern argument, a runtime error will occur. The ``traverse`` pattern +should not contain segment markers that do not exist in the ``path``. -Note that the ``traverse`` argument is ignored when attached to a -route that has a ``*traverse`` remainder marker in its pattern. +Note that the ``traverse`` argument is ignored when attached to a route that +has a ``*traverse`` remainder marker in its pattern. -Traversal will begin at the root object implied by this route (either -the global root, or the object returned by the ``factory`` associated -with this route). +Traversal will begin at the root object implied by this route (either the +global root, or the object returned by the ``factory`` associated with this +route). + +.. index:: + pair: hybrid applications; global views Making Global Views Match +++++++++++++++++++++++++ -By default, only view configurations that mention a ``route_name`` -will be found during view lookup when a route that has a ``*traverse`` -in its pattern matches. You can allow views without a ``route_name`` -attribute to match a route by adding the ``use_global_views`` flag to -the route definition. For example, the ``myproject.views.bazbuz`` -view below will be found if the route named ``abc`` below is matched -and the ``PATH_INFO`` is ``/abc/bazbuz``, even though the view -configuration statement does not have the ``route_name="abc"`` +By default, only view configurations that mention a ``route_name`` will be +found during view lookup when a route that has a ``*traverse`` in its pattern +matches. You can allow views without a ``route_name`` attribute to match a +route by adding the ``use_global_views`` flag to the route definition. For +example, the ``myproject.views.bazbuz`` view below will be found if the route +named ``abc`` below is matched and the ``PATH_INFO`` is ``/abc/bazbuz``, even +though the view configuration statement does not have the ``route_name="abc"`` attribute. .. code-block:: python @@ -420,6 +410,7 @@ attribute. config.add_view('myproject.views.bazbuz', name='bazbuz') .. index:: + pair: hybrid applications; *subpath single: route subpath single: subpath (route) @@ -431,103 +422,127 @@ Using ``*subpath`` in a Route Pattern There are certain extremely rare cases when you'd like to influence the traversal :term:`subpath` when a route matches without actually performing traversal. For instance, the :func:`pyramid.wsgi.wsgiapp2` decorator and the -:class:`pyramid.view.static` helper attempt to compute ``PATH_INFO`` from the -request's subpath, so it's useful to be able to influence this value. +:class:`pyramid.static.static_view` helper attempt to compute ``PATH_INFO`` +from the request's subpath when its ``use_subpath`` argument is ``True``, so +it's useful to be able to influence this value. -When ``*subpath`` exists in a pattern, no path is actually traversed, -but the traversal algorithm will return a :term:`subpath` list implied -by the capture value of ``*subpath``. You'll see this pattern most -commonly in route declarations that look like this: +When ``*subpath`` exists in a pattern, no path is actually traversed, but the +traversal algorithm will return a :term:`subpath` list implied by the capture +value of ``*subpath``. You'll see this pattern most commonly in route +declarations that look like this: .. code-block:: python :linenos: + from pyramid.static import static_view + + www = static_view('mypackage:static', use_subpath=True) + config.add_route('static', '/static/*subpath') - config.add_view('mypackage.views.static_view', route_name='static') + config.add_view(www, route_name='static') + +``mypackage.views.www`` is an instance of :class:`pyramid.static.static_view`. +This effectively tells the static helper to traverse everything in the subpath +as a filename. -Where ``mypackage.views.static_view`` is an instance of -:class:`pyramid.view.static`. This effectively tells the static helper to -traverse everything in the subpath as a filename. -Corner Cases ------------- +.. index:: + pair: hybrid URLs; generating + +.. _generating_hybrid_urls: -A number of corner case "gotchas" exist when using a hybrid -application. We'll detail them here. +Generating Hybrid URLs +---------------------- -Registering a Default View for a Route That Has a ``view`` Attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. versionadded:: 1.5 -.. warning:: As of :app:`Pyramid` 1.1 this section is slated to be removed in - a later documentation release because the the ability to add views - directly to the :term:`route configuration` by passing a ``view`` argument - to ``add_route`` has been deprecated. +The :meth:`pyramid.request.Request.resource_url` method and the +:meth:`pyramid.request.Request.resource_path` method both accept optional +keyword arguments that make it easier to generate route-prefixed URLs that +contain paths to traversal resources: ``route_name``, ``route_kw``, and +``route_remainder_name``. -It is an error to provide *both* a ``view`` argument to a :term:`route -configuration` *and* a :term:`view configuration` which names a -``route_name`` that has no ``name`` value or the empty ``name`` value. For -example, this pair of declarations will generate a conflict error at startup -time. +Any route that has a pattern that contains a ``*remainder`` pattern (any +stararg remainder pattern, such as ``*traverse``, ``*subpath``, or ``*fred``) +can be used as the target name for ``request.resource_url(..., route_name=)`` +and ``request.resource_path(..., route_name=)``. + +For example, let's imagine you have a route defined in your Pyramid application +like so: .. code-block:: python - :linenos: - config.add_route('home', '{foo}/{bar}/*traverse', - view='myproject.views.home') - config.add_view('myproject.views.another', route_name='home') + config.add_route('mysection', '/mysection*traverse') -This is because the ``view`` argument to the -:meth:`~pyramid.config.Configurator.add_route` above is an *implicit* -default view when that route matches. ``add_route`` calls don't *need* to -supply a view attribute. For example, this ``add_route`` call: +If you'd like to generate the URL ``http://example.com/mysection/a/``, you can +use the following incantation, assuming that the variable ``a`` below points to +a resource that is a child of the root with a ``__name__`` of ``a``: .. code-block:: python - :linenos: - config.add_route('home', '{foo}/{bar}/*traverse', - view='myproject.views.home') + request.resource_url(a, route_name='mysection') -Can also be spelled like so: +You can generate only the path portion ``/mysection/a/`` assuming the same: .. code-block:: python - :linenos: - config.add_route('home', '{foo}/{bar}/*traverse') - config.add_view('myproject.views.home', route_name='home') + request.resource_path(a, route_name='mysection') -The two spellings are logically equivalent. In fact, the former is just a -syntactical shortcut for the latter. +The path is virtual host aware, so if the ``X-Vhm-Root`` environment variable +is present in the request, and it's set to ``/a``, the above call to +``request.resource_url`` would generate ``http://example.com/mysection/``, and +the above call to ``request.resource_path`` would generate ``/mysection/``. See +:ref:`virtual_root_support` for more information. -Binding Extra Views Against a Route Configuration that Doesn't Have a ``*traverse`` Element In Its Pattern -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If the route you're trying to use needs simple dynamic part values to be filled +in to succesfully generate the URL, you can pass these as the ``route_kw`` +argument to ``resource_url`` and ``resource_path``. For example, assuming that +the route definition is like so: -Here's another corner case that just makes no sense: +.. code-block:: python + + config.add_route('mysection', '/{id}/mysection*traverse') + +You can pass ``route_kw`` in to fill in ``{id}`` above: .. code-block:: python - :linenos: - config.add_route('abc', '/abc', view='myproject.views.abc') - config.add_view('myproject.views.bazbuz', name='bazbuz', - route_name='abc') + request.resource_url(a, route_name='mysection', route_kw={'id':'1'}) -The above view declaration is useless, because it will never be matched when -the route it references has matched. Only the view associated with the route -itself (``myproject.views.abc``) will ever be invoked when the route matches, -because the default view is always invoked when a route matches and when no -post-match traversal is performed. +If you pass ``route_kw`` but do not pass ``route_name``, ``route_kw`` will be +ignored. -To make the above view declaration useful, the special ``*traverse`` -token must end the route's pattern. For example: +By default this feature works by calling ``route_url`` under the hood, and +passing the value of the resource path to that function as ``traverse``. If +your route has a different ``*stararg`` remainder name (such as ``*subpath``), +you can tell ``resource_url`` or ``resource_path`` to use that instead of +``traverse`` by passing ``route_remainder_name``. For example, if you have the +following route: .. code-block:: python - :linenos: - config.add_route('abc', '/abc/*traverse', view='myproject.views.abc') - config.add_view('myproject.views.bazbuz', name='bazbuz', - route_name='abc') + config.add_route('mysection', '/mysection*subpath') + +You can fill in the ``*subpath`` value using ``resource_url`` by doing: + +.. code-block:: python + + request.resource_path(a, route_name='mysection', + route_remainder_name='subpath') + +If you pass ``route_remainder_name`` but do not pass ``route_name``, +``route_remainder_name`` will be ignored. + +If you try to use ``resource_path`` or ``resource_url`` when the ``route_name`` +argument points at a route that does not have a remainder stararg, an error +will not be raised, but the generated URL will not contain any remainder +information either. + +All other values that are normally passable to ``resource_path`` and +``resource_url`` (such as ``query``, ``anchor``, ``host``, ``port``, and +positional elements) work as you might expect in this configuration. -With the above configuration, the ``myproject.views.bazbuz`` view will -be invoked when the request URI is ``/abc/bazbuz``, assuming there is -no object contained by the root object with the key ``bazbuz``. A -different request URI, such as ``/abc/foo/bar``, would invoke the -default ``myproject.views.abc`` view. +Note that this feature is incompatible with the ``__resource_url__`` feature +(see :ref:`overriding_resource_url_generation`) implemented on resource +objects. Any ``__resource_url__`` supplied by your resource will be ignored +when you pass ``route_name``. diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index c21a19b5b..014f314ad 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -9,16 +9,16 @@ Internationalization and Localization ===================================== -:term:`Internationalization` (i18n) is the act of creating software -with a user interface that can potentially be displayed in more than -one language or cultural context. :term:`Localization` (l10n) is the -process of displaying the user interface of an internationalized -application in a *particular* language or cultural context. - -:app:`Pyramid` offers internationalization and localization -subsystems that can be used to translate the text of buttons, error -messages and other software- and template-defined values into the -native language of a user of your application. +:term:`Internationalization` (i18n) is the act of creating software with a user +interface that can potentially be displayed in more than one language or +cultural context. :term:`Localization` (l10n) is the process of displaying the +user interface of an internationalized application in a *particular* language +or cultural context. + +:app:`Pyramid` offers internationalization and localization subsystems that can +be used to translate the text of buttons, error messages, and other software- +and template-defined values into the native language of a user of your +application. .. index:: single: translation string @@ -29,15 +29,15 @@ native language of a user of your application. Creating a Translation String ----------------------------- -While you write your software, you can insert specialized markup into -your Python code that makes it possible for the system to translate -text values into the languages used by your application's users. This -markup creates a :term:`translation string`. A translation string is -an object that behaves mostly like a normal Unicode object, except that -it also carries around extra information related to its job as part of -the :app:`Pyramid` translation machinery. +While you write your software, you can insert specialized markup into your +Python code that makes it possible for the system to translate text values into +the languages used by your application's users. This markup creates a +:term:`translation string`. A translation string is an object that behaves +mostly like a normal Unicode object, except that it also carries around extra +information related to its job as part of the :app:`Pyramid` translation +machinery. -Using The ``TranslationString`` Class +Using the ``TranslationString`` Class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The most primitive way to create a translation string is to use the @@ -53,17 +53,17 @@ This creates a Unicode-like object that is a TranslationString. .. note:: - For people more familiar with :term:`Zope` i18n, a TranslationString - is a lot like a ``zope.i18nmessageid.Message`` object. It is not a - subclass, however. For people more familiar with :term:`Pylons` or - :term:`Django` i18n, using a TranslationString is a lot like using - "lazy" versions of related gettext APIs. + For people more familiar with :term:`Zope` i18n, a TranslationString is a + lot like a ``zope.i18nmessageid.Message`` object. It is not a subclass, + however. For people more familiar with :term:`Pylons` or :term:`Django` + i18n, using a TranslationString is a lot like using "lazy" versions of + related gettext APIs. -The first argument to :class:`~pyramid.i18n.TranslationString` is -the ``msgid``; it is required. It represents the key into the -translation mappings provided by a particular localization. The -``msgid`` argument must be a Unicode object or an ASCII string. The -msgid may optionally contain *replacement markers*. For instance: +The first argument to :class:`~pyramid.i18n.TranslationString` is the +``msgid``; it is required. It represents the key into the translation mappings +provided by a particular localization. The ``msgid`` argument must be a Unicode +object or an ASCII string. The msgid may optionally contain *replacement +markers*. For instance: .. code-block:: python :linenos: @@ -71,10 +71,9 @@ msgid may optionally contain *replacement markers*. For instance: from pyramid.i18n import TranslationString ts = TranslationString('Add ${number}') -Within the string above, ``${number}`` is a replacement marker. It -will be replaced by whatever is in the *mapping* for a translation -string. The mapping may be supplied at the same time as the -replacement marker itself: +Within the string above, ``${number}`` is a replacement marker. It will be +replaced by whatever is in the *mapping* for a translation string. The mapping +may be supplied at the same time as the replacement marker itself: .. code-block:: python :linenos: @@ -82,14 +81,14 @@ replacement marker itself: from pyramid.i18n import TranslationString ts = TranslationString('Add ${number}', mapping={'number':1}) -Any number of replacement markers can be present in the msgid value, -any number of times. Only markers which can be replaced by the values -in the *mapping* will be replaced at translation time. The others -will not be interpolated and will be output literally. +Any number of replacement markers can be present in the msgid value, any number +of times. Only markers which can be replaced by the values in the *mapping* +will be replaced at translation time. The others will not be interpolated and +will be output literally. A translation string should also usually carry a *domain*. The domain -represents a translation category to disambiguate it from other -translations of the same msgid, in case they conflict. +represents a translation category to disambiguate it from other translations of +the same msgid, in case they conflict. .. code-block:: python :linenos: @@ -98,13 +97,12 @@ translations of the same msgid, in case they conflict. ts = TranslationString('Add ${number}', mapping={'number':1}, domain='form') -The above translation string named a domain of ``form``. A -:term:`translator` function will often use the domain to locate the -right translator file on the filesystem which contains translations -for a given domain. In this case, if it were trying to translate -our msgid to German, it might try to find a translation from a -:term:`gettext` file within a :term:`translation directory` like this -one: +The above translation string named a domain of ``form``. A :term:`translator` +function will often use the domain to locate the right translator file on the +filesystem which contains translations for a given domain. In this case, if it +were trying to translate our msgid to German, it might try to find a +translation from a :term:`gettext` file within a :term:`translation directory` +like this one: .. code-block:: text @@ -113,14 +111,13 @@ one: In other words, it would want to take translations from the ``form.mo`` translation file in the German language. -Finally, the TranslationString constructor accepts a ``default`` -argument. If a ``default`` argument is supplied, it replaces usages -of the ``msgid`` as the *default value* for the translation string. -When ``default`` is ``None``, the ``msgid`` value passed to a -TranslationString is used as an implicit message identifier. Message -identifiers are matched with translations in translation files, so it -is often useful to create translation strings with "opaque" message -identifiers unrelated to their default text: +Finally, the TranslationString constructor accepts a ``default`` argument. If +a ``default`` argument is supplied, it replaces usages of the ``msgid`` as the +*default value* for the translation string. When ``default`` is ``None``, the +``msgid`` value passed to a TranslationString is used as an implicit message +identifier. Message identifiers are matched with translations in translation +files, so it is often useful to create translation strings with "opaque" +message identifiers unrelated to their default text: .. code-block:: python :linenos: @@ -129,8 +126,7 @@ identifiers unrelated to their default text: ts = TranslationString('add-number', default='Add ${number}', domain='form', mapping={'number':1}) -When default text is used, Default text objects may contain -replacement values. +When default text is used, Default text objects may contain replacement values. .. index:: single: translation string factory @@ -139,195 +135,167 @@ Using the ``TranslationStringFactory`` Class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Another way to generate a translation string is to use the -:attr:`~pyramid.i18n.TranslationStringFactory` object. This object -is a *translation string factory*. Basically a translation string -factory presets the ``domain`` value of any :term:`translation string` -generated by using it. For example: +:attr:`~pyramid.i18n.TranslationStringFactory` object. This object is a +*translation string factory*. Basically a translation string factory presets +the ``domain`` value of any :term:`translation string` generated by using it. +For example: .. code-block:: python :linenos: from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('pyramid') - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}) + ts = _('add-number', default='Add ${number}', mapping={'number':1}) -.. note:: We assigned the translation string factory to the name - ``_``. This is a convention which will be supported by translation - file generation tools. +.. note:: We assigned the translation string factory to the name ``_``. This + is a convention which will be supported by translation file generation + tools. After assigning ``_`` to the result of a -:func:`~pyramid.i18n.TranslationStringFactory`, the subsequent result -of calling ``_`` will be a :class:`~pyramid.i18n.TranslationString` -instance. Even though a ``domain`` value was not passed to ``_`` (as -would have been necessary if the -:class:`~pyramid.i18n.TranslationString` constructor were used instead -of a translation string factory), the ``domain`` attribute of the -resulting translation string will be ``pyramid``. As a result, the -previous code example is completely equivalent (except for spelling) -to: +:func:`~pyramid.i18n.TranslationStringFactory`, the subsequent result of +calling ``_`` will be a :class:`~pyramid.i18n.TranslationString` instance. +Even though a ``domain`` value was not passed to ``_`` (as would have been +necessary if the :class:`~pyramid.i18n.TranslationString` constructor were used +instead of a translation string factory), the ``domain`` attribute of the +resulting translation string will be ``pyramid``. As a result, the previous +code example is completely equivalent (except for spelling) to: .. code-block:: python :linenos: from pyramid.i18n import TranslationString as _ - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}, + ts = _('add-number', default='Add ${number}', mapping={'number':1}, domain='pyramid') -You can set up your own translation string factory much like the one -provided above by using the -:class:`~pyramid.i18n.TranslationStringFactory` class. For example, -if you'd like to create a translation string factory which presets the -``domain`` value of generated translation strings to ``form``, you'd -do something like this: +You can set up your own translation string factory much like the one provided +above by using the :class:`~pyramid.i18n.TranslationStringFactory` class. For +example, if you'd like to create a translation string factory which presets the +``domain`` value of generated translation strings to ``form``, you'd do +something like this: .. code-block:: python :linenos: from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('form') - ts = _('Add ${number}', msgid='add-number', mapping={'number':1}) + ts = _('add-number', default='Add ${number}', mapping={'number':1}) -Creating a unique domain for your application via a translation string -factory is best practice. Using your own unique translation domain -allows another person to reuse your application without needing to -merge your translation files with his own. Instead, he can just -include your package's :term:`translation directory` via the -:meth:`pyramid.config.Configurator.add_translation_dirs` -method. +Creating a unique domain for your application via a translation string factory +is best practice. Using your own unique translation domain allows another +person to reuse your application without needing to merge your translation +files with their own. Instead they can just include your package's +:term:`translation directory` via the +:meth:`pyramid.config.Configurator.add_translation_dirs` method. .. note:: For people familiar with Zope internationalization, a TranslationStringFactory is a lot like a - ``zope.i18nmessageid.MessageFactory`` object. It is not a - subclass, however. + ``zope.i18nmessageid.MessageFactory`` object. It is not a subclass, + however. .. index:: single: gettext single: translation directories -Working With ``gettext`` Translation Files +Working with ``gettext`` Translation Files ------------------------------------------ -The basis of :app:`Pyramid` translation services is -GNU :term:`gettext`. Once your application source code files and templates -are marked up with translation markers, you can work on translations -by creating various kinds of gettext files. +The basis of :app:`Pyramid` translation services is GNU :term:`gettext`. Once +your application source code files and templates are marked up with translation +markers, you can work on translations by creating various kinds of gettext +files. .. note:: - The steps a developer must take to work with :term:`gettext` - :term:`message catalog` files within a :app:`Pyramid` - application are very similar to the steps a :term:`Pylons` - developer must take to do the same. See the `Pylons - internationalization documentation - <http://wiki.pylonshq.com/display/pylonsdocs/Internationalization+and+Localization>`_ - for more information. + The steps a developer must take to work with :term:`gettext` :term:`message + catalog` files within a :app:`Pyramid` application are very similar to the + steps a :term:`Pylons` developer must take to do the same. See the + :ref:`Pylons Internationalization and Localization documentation + <pylonswebframework:i18n>` for more information. -GNU gettext uses three types of files in the translation framework, -``.pot`` files, ``.po`` files and ``.mo`` files. +GNU gettext uses three types of files in the translation framework, ``.pot`` +files, ``.po`` files, and ``.mo`` files. ``.pot`` (Portable Object Template) files - A ``.pot`` file is created by a program which searches through your - project's source code and which picks out every :term:`message - identifier` passed to one of the ``_()`` functions - (eg. :term:`translation string` constructions). The list of all - message identifiers is placed into a ``.pot`` file, which serves as - a template for creating ``.po`` files. + A ``.pot`` file is created by a program which searches through your project's + source code and which picks out every :term:`message identifier` passed to + one of the ``_()`` functions (e.g., :term:`translation string` + constructions). The list of all message identifiers is placed into a ``.pot`` + file, which serves as a template for creating ``.po`` files. ``.po`` (Portable Object) files - The list of messages in a ``.pot`` file are translated by a human to - a particular language; the result is saved as a ``.po`` file. + The list of messages in a ``.pot`` file are translated by a human to a + particular language; the result is saved as a ``.po`` file. ``.mo`` (Machine Object) files - A ``.po`` file is turned into a machine-readable binary file, which - is the ``.mo`` file. Compiling the translations to machine code - makes the localized program run faster. + A ``.po`` file is turned into a machine-readable binary file, which is the + ``.mo`` file. Compiling the translations to machine code makes the + localized program start faster. The tools for working with :term:`gettext` translation files related to a -:app:`Pyramid` application is :term:`Babel` and :term:`Lingua`. Lingua is a -Balel extension that provides support for scraping i18n references out of -Python and Chameleon files. +:app:`Pyramid` application are :term:`Lingua` and :term:`Gettext`. Lingua can +scrape i18n references out of Python and Chameleon files and create the +``.pot`` file. Gettext includes ``msgmerge`` tool to update a ``.po`` file from +an updated ``.pot`` file and ``msgfmt`` to compile ``.po`` files to ``.mo`` +files. .. index:: - single: Babel + single: Gettext single: Lingua .. _installing_babel: -Installing Babel and Lingua -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Installing Lingua and Gettext +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In order for the commands related to working with ``gettext`` translation -files to work properly, you will need to have :term:`Babel` and -:term:`Lingua` installed into the same environment in which :app:`Pyramid` is -installed. +In order for the commands related to working with ``gettext`` translation files +to work properly, you will need to have :term:`Lingua` and :term:`Gettext` +installed into the same environment in which :app:`Pyramid` is installed. Installation on UNIX ++++++++++++++++++++ -If the :term:`virtualenv` into which you've installed your :app:`Pyramid` -application lives in ``/my/virtualenv``, you can install Babel and Lingua -like so: +Gettext is often already installed on UNIX systems. You can check if it is +installed by testing if the ``msgfmt`` command is available. If it is not +available you can install it through the packaging system from your OS; the +package name is almost always ``gettext``. For example on a Debian or Ubuntu +system run this command: -.. code-block:: text +.. code-block:: bash - $ cd /my/virtualenv - $ bin/easy_install Babel lingua + $ sudo apt-get install gettext -Installation on Windows -+++++++++++++++++++++++ - -If the :term:`virtualenv` into which you've installed your :app:`Pyramid` -application lives in ``C:\my\virtualenv``, you can install Babel and Lingua +Installing Lingua is done with the Python packaging tools. If the +:term:`virtual environment` into which you've installed your :app:`Pyramid` +application lives at the environment variable ``$VENV``, you can install Lingua like so: -.. code-block:: text - - C> cd \my\virtualenv - C> Scripts\easy_install Babel lingua +.. code-block:: bash -.. index:: - single: Babel; message extractors - single: Lingua + $ $VENV/bin/pip install lingua -Changing the ``setup.py`` -+++++++++++++++++++++++++ +Installation on Windows ++++++++++++++++++++++++ -You need to add a few boilerplate lines to your application's ``setup.py`` -file in order to properly generate :term:`gettext` files from your -application. +There are several ways to install Gettext on Windows: it is included in the +`Cygwin <http://www.cygwin.com/>`_ collection, or you can use the `installer +from the GnuWin32 <http://gnuwin32.sourceforge.net/packages/gettext.htm>`_, or +compile it yourself. Make sure the installation path is added to your +``$PATH``. -.. note:: See :ref:`project_narr` to learn about about the - composition of an application's ``setup.py`` file. +Installing Lingua is done with the Python packaging tools. If the +:term:`virtual environment` into which you've installed your :app:`Pyramid` +application lives at the environment variable ``%VENV%``, you can install +Lingua like so: -In particular, add the ``Babel`` and ``lingua`` distributions to the -``install_requires`` list and insert a set of references to :term:`Babel` -*message extractors* within the call to :func:`setuptools.setup` inside your -application's ``setup.py`` file: +.. code-block:: doscon -.. code-block:: python - :linenos: + C> %VENV%\Scripts\pip install lingua - setup(name="mypackage", - # ... - install_requires = [ - # ... - 'Babel', - 'lingua', - ], - message_extractors = { '.': [ - ('**.py', 'lingua_python', None ), - ('**.pt', 'lingua_xml', None ), - ]}, - ) - -The ``message_extractors`` stanza placed into the ``setup.py`` file causes -the :term:`Babel` message catalog extraction machinery to also consider -``*.pt`` files when doing message id extraction. .. index:: pair: extracting; messages @@ -337,87 +305,19 @@ the :term:`Babel` message catalog extraction machinery to also consider Extracting Messages from Code and Templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Once Babel and Lingua are installed and your application's ``setup.py`` file -has the correct message extractor references, you may extract a message -catalog template from the code and :term:`Chameleon` templates which reside -in your :app:`Pyramid` application. You run a ``setup.py`` command to -extract the messages: +Once Lingua is installed, you may extract a message catalog template from the +code and :term:`Chameleon` templates which reside in your :app:`Pyramid` +application. You run a ``pot-create`` command to extract the messages: -.. code-block:: text +.. code-block:: bash - $ cd /place/where/myapplication/setup.py/lives + $ cd /file/path/to/myapplication_setup.py $ mkdir -p myapplication/locale - $ python setup.py extract_messages - -The message catalog ``.pot`` template will end up in: + $ $VENV/bin/pot-create -o myapplication/locale/myapplication.pot src +The message catalog ``.pot`` template will end up in ``myapplication/locale/myapplication.pot``. -Translation Domains -+++++++++++++++++++ - -The name ``myapplication`` above in the filename ``myapplication.pot`` -denotes the :term:`translation domain` of the translations that must -be performed to localize your application. By default, the -translation domain is the :term:`project` name of your -:app:`Pyramid` application. - -To change the translation domain of the extracted messages in your -project, edit the ``setup.cfg`` file of your application, The default -``setup.cfg`` file of a Paster-generated :app:`Pyramid` application -has stanzas in it that look something like the following: - -.. code-block:: ini - :linenos: - - [compile_catalog] - directory = myproject/locale - domain = MyProject - statistics = true - - [extract_messages] - add_comments = TRANSLATORS: - output_file = myproject/locale/MyProject.pot - width = 80 - - [init_catalog] - domain = MyProject - input_file = myproject/locale/MyProject.pot - output_dir = myproject/locale - - [update_catalog] - domain = MyProject - input_file = myproject/locale/MyProject.pot - output_dir = myproject/locale - previous = true - -In the above example, the project name is ``MyProject``. To indicate -that you'd like the domain of your translations to be ``mydomain`` -instead, change the ``setup.cfg`` file stanzas to look like so: - -.. code-block:: ini - :linenos: - - [compile_catalog] - directory = myproject/locale - domain = mydomain - statistics = true - - [extract_messages] - add_comments = TRANSLATORS: - output_file = myproject/locale/mydomain.pot - width = 80 - - [init_catalog] - domain = mydomain - input_file = myproject/locale/mydomain.pot - output_dir = myproject/locale - - [update_catalog] - domain = mydomain - input_file = myproject/locale/mydomain.pot - output_dir = myproject/locale - previous = true .. index:: pair: initializing; message catalog @@ -426,30 +326,28 @@ Initializing a Message Catalog File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once you've extracted messages into a ``.pot`` file (see -:ref:`extracting_messages`), to begin localizing the messages present -in the ``.pot`` file, you need to generate at least one ``.po`` file. -A ``.po`` file represents translations of a particular set of messages -to a particular locale. Initialize a ``.po`` file for a specific -locale from a pre-generated ``.pot`` template by using the ``setup.py -init_catalog`` command: +:ref:`extracting_messages`), to begin localizing the messages present in the +``.pot`` file, you need to generate at least one ``.po`` file. A ``.po`` file +represents translations of a particular set of messages to a particular locale. +Initialize a ``.po`` file for a specific locale from a pre-generated ``.pot`` +template by using the ``msginit`` command from Gettext: -.. code-block:: text +.. code-block:: bash - $ cd /place/where/myapplication/setup.py/lives - $ python setup.py init_catalog -l es - -By default, the message catalog ``.po`` file will end up in: + $ cd /file/path/to/myapplication_setup.py + $ cd myapplication/locale + $ mkdir -p es/LC_MESSAGES + $ msginit -l es -o es/LC_MESSAGES/myapplication.po +This will create a new message catalog ``.po`` file in ``myapplication/locale/es/LC_MESSAGES/myapplication.po``. -Once the file is there, it can be worked on by a human translator. -One tool which may help with this is `Poedit -<http://www.poedit.net/>`_. +Once the file is there, it can be worked on by a human translator. One tool +which may help with this is `Poedit <http://www.poedit.net/>`_. -Note that :app:`Pyramid` itself ignores the existence of all -``.po`` files. For a running application to have translations -available, a ``.mo`` file must exist. See -:ref:`compiling_message_catalog`. +Note that :app:`Pyramid` itself ignores the existence of all ``.po`` files. +For a running application to have translations available, a ``.mo`` file must +exist. See :ref:`compiling_message_catalog`. .. index:: pair: updating; message catalog @@ -457,18 +355,19 @@ available, a ``.mo`` file must exist. See Updating a Catalog File ~~~~~~~~~~~~~~~~~~~~~~~ -If more translation strings are added to your application, or -translation strings change, you will need to update existing ``.po`` -files based on changes to the ``.pot`` file, so that the new and -changed messages can also be translated or re-translated. +If more translation strings are added to your application, or translation +strings change, you will need to update existing ``.po`` files based on changes +to the ``.pot`` file, so that the new and changed messages can also be +translated or re-translated. -First, regenerate the ``.pot`` file as per :ref:`extracting_messages`. -Then use the ``setup.py update_catalog`` command. +First, regenerate the ``.pot`` file as per :ref:`extracting_messages`. Then use +the ``msgmerge`` command from Gettext. -.. code-block:: text +.. code-block:: bash - $ cd /place/where/myapplication/setup.py/lives - $ python setup.py update_catalog + $ cd /file/path/to/myapplication_setup.py + $ cd myapplication/locale + $ msgmerge --update es/LC_MESSAGES/myapplication.po myapplication.pot .. index:: pair: compiling; message catalog @@ -478,42 +377,46 @@ Then use the ``setup.py update_catalog`` command. Compiling a Message Catalog File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Finally, to prepare an application for performing actual runtime -translations, compile ``.po`` files to ``.mo`` files: +Finally, to prepare an application for performing actual runtime translations, +compile ``.po`` files to ``.mo`` files using the ``msgfmt`` command from +Gettext: -.. code-block:: text +.. code-block:: bash - $ cd /place/where/myapplication/setup.py/lives - $ python setup.py compile_catalog + $ cd /file/path/to/myapplication_setup.py + $ msgfmt -o myapplication/locale/es/LC_MESSAGES/myapplication.mo \ + myapplication/locale/es/LC_MESSAGES/myapplication.po -This will create a ``.mo`` file for each ``.po`` file in your -application. As long as the :term:`translation directory` in which -the ``.mo`` file ends up in is configured into your application, these -translations will be available to :app:`Pyramid`. +This will create a ``.mo`` file for each ``.po`` file in your application. As +long as the :term:`translation directory` in which the ``.mo`` file ends up in +is configured into your application (see +:ref:`adding_a_translation_directory`), these translations will be available to +:app:`Pyramid`. .. index:: single: localizer - single: get_localizer + single: translation + single: pluralization Using a Localizer ----------------- A :term:`localizer` is an object that allows you to perform translation or pluralization "by hand" in an application. You may use the -:func:`pyramid.i18n.get_localizer` function to obtain a :term:`localizer`. -This function will return either the localizer object implied by the active -:term:`locale negotiator` or a default localizer object if no explicit locale -negotiator is registered. +:attr:`pyramid.request.Request.localizer` attribute to obtain a +:term:`localizer`. The localizer object will be configured to produce +translations implied by the active :term:`locale negotiator`, or a default +localizer object if no explicit locale negotiator is registered. .. code-block:: python :linenos: - from pyramid.i18n import get_localizer - def aview(request): - locale = get_localizer(request) + localizer = request.localizer + +.. note:: -.. note:: If you need to create a localizer for a locale use the + If you need to create a localizer for a locale, use the :func:`pyramid.i18n.make_localizer` function. .. index:: @@ -525,34 +428,33 @@ Performing a Translation ~~~~~~~~~~~~~~~~~~~~~~~~ A :term:`localizer` has a ``translate`` method which accepts either a -:term:`translation string` or a Unicode string and which returns a -Unicode object representing the translation. So, generating a -translation in a view component of an application might look like so: +:term:`translation string` or a Unicode string and which returns a Unicode +object representing the translation. Generating a translation in a view +component of an application might look like so: .. code-block:: python :linenos: - from pyramid.i18n import get_localizer from pyramid.i18n import TranslationString ts = TranslationString('Add ${number}', mapping={'number':1}, domain='pyramid') def aview(request): - localizer = get_localizer(request) + localizer = request.localizer translated = localizer.translate(ts) # translation string # ... use translated ... -The :func:`~pyramid.i18n.get_localizer` function will return a -:class:`pyramid.i18n.Localizer` object bound to the locale name -represented by the request. The translation returned from its -:meth:`pyramid.i18n.Localizer.translate` method will depend on the -``domain`` attribute of the provided translation string as well as the +The ``request.localizer`` attribute will be a :class:`pyramid.i18n.Localizer` +object bound to the locale name represented by the request. The translation +returned from its :meth:`pyramid.i18n.Localizer.translate` method will depend +on the ``domain`` attribute of the provided translation string as well as the locale of the localizer. -.. note:: If you're using :term:`Chameleon` templates, you don't need - to pre-translate translation strings this way. See - :ref:`chameleon_translation_strings`. +.. note:: + + If you're using :term:`Chameleon` templates, you don't need to pre-translate + translation strings this way. See :ref:`chameleon_translation_strings`. .. index:: single: pluralizing (i18n) @@ -562,8 +464,7 @@ locale of the localizer. Performing a Pluralization ~~~~~~~~~~~~~~~~~~~~~~~~~~ -A :term:`localizer` has a ``pluralize`` method with the following -signature: +A :term:`localizer` has a ``pluralize`` method with the following signature: .. code-block:: python :linenos: @@ -571,32 +472,60 @@ signature: def pluralize(singular, plural, n, domain=None, mapping=None): ... -The ``singular`` and ``plural`` arguments should each be a Unicode -value representing a :term:`message identifier`. ``n`` should be an -integer. ``domain`` should be a :term:`translation domain`, and -``mapping`` should be a dictionary that is used for *replacement -value* interpolation of the translated string. If ``n`` is plural -for the current locale, ``pluralize`` will return a Unicode -translation for the message id ``plural``, otherwise it will return a -Unicode translation for the message id ``singular``. - -The arguments provided as ``singular`` and/or ``plural`` may also be -:term:`translation string` objects, but the domain and mapping -information attached to those objects is ignored. +The simplest case is the ``singular`` and ``plural`` arguments being passed as +Unicode literals. This returns the appropriate literal according to the locale +pluralization rules for the number ``n``, and interpolates ``mapping``. .. code-block:: python :linenos: - from pyramid.i18n import get_localizer - def aview(request): - localizer = get_localizer(request) + localizer = request.localizer translated = localizer.pluralize('Item', 'Items', 1, 'mydomain') # ... use translated ... +However, for support of other languages, the ``singular`` argument should be a +Unicode value representing a :term:`message identifier`. In this case the +``plural`` value is ignored. ``domain`` should be a :term:`translation domain`, +and ``mapping`` should be a dictionary that is used for *replacement value* +interpolation of the translated string. + +The value of ``n`` will be used to find the appropriate plural form for the +current language, and ``pluralize`` will return a Unicode translation for the +message id ``singular``. The message file must have defined ``singular`` as a +translation with plural forms. + +The argument provided as ``singular`` may be a :term:`translation string` +object, but the domain and mapping information attached is ignored. + +.. code-block:: python + :linenos: + + def aview(request): + localizer = request.localizer + num = 1 + translated = localizer.pluralize('item_plural', '${number} items', + num, 'mydomain', mapping={'number':num}) + +The corresponding message catalog must have language plural definitions and +plural alternatives set. + +.. code-block:: text + :linenos: + + "Plural-Forms: nplurals=3; plural=n==0 ? 0 : n==1 ? 1 : 2;" + + msgid "item_plural" + msgid_plural "" + msgstr[0] "No items" + msgstr[1] "${number} item" + msgstr[2] "${number} items" + +More information on complex plurals can be found in the `gettext documentation +<https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html>`_. + .. index:: single: locale name - single: get_locale_name single: negotiate_locale_name .. _obtaining_the_locale_name: @@ -605,28 +534,25 @@ Obtaining the Locale Name for a Request --------------------------------------- You can obtain the locale name related to a request by using the -:func:`pyramid.i18n.get_locale_name` function. +:func:`pyramid.request.Request.locale_name` attribute of the request. .. code-block:: python :linenos: - from pyramid.i18n import get_locale_name - def aview(request): - locale_name = get_locale_name(request) + locale_name = request.locale_name -This returns the locale name negotiated by the currently active -:term:`locale negotiator` or the :term:`default locale name` if the -locale negotiator returns ``None``. You can change the default locale -name by changing the ``default_locale_name`` setting; see -:ref:`default_locale_name_setting`. +The locale name of a request is dynamically computed; it will be the locale +name negotiated by the currently active :term:`locale negotiator`, or the +:term:`default locale name` if the locale negotiator returns ``None``. You can +change the default locale name by changing the ``pyramid.default_locale_name`` +setting. See :ref:`default_locale_name_setting`. -Once :func:`~pyramid.i18n.get_locale_name` is first run, the locale -name is stored on the request object. Subsequent calls to -:func:`~pyramid.i18n.get_locale_name` will return the stored locale -name without invoking the :term:`locale negotiator`. To avoid this -caching, you can use the :func:`pyramid.i18n.negotiate_locale_name` -function: +Once :func:`~pyramid.request.Request.locale_name` is first run, the locale name +is stored on the request object. Subsequent calls to +:func:`~pyramid.request.Request.locale_name` will return the stored locale name +without invoking the :term:`locale negotiator`. To avoid this caching, you can +use the :func:`pyramid.i18n.negotiate_locale_name` function: .. code-block:: python :linenos: @@ -642,15 +568,13 @@ You can also obtain the locale name related to a request using the .. code-block:: python :linenos: - from pyramid.i18n import get_localizer - def aview(request): - localizer = get_localizer(request) + localizer = request.localizer locale_name = localizer.locale_name -Obtaining the locale name as an attribute of a localizer is equivalent -to obtaining a locale name by calling the -:func:`~pyramid.i18n.get_locale_name` function. +Obtaining the locale name as an attribute of a localizer is equivalent to +obtaining a locale name by asking for the +:func:`~pyramid.request.Request.locale_name` attribute. .. index:: single: date and currency formatting (i18n) @@ -659,29 +583,26 @@ to obtaining a locale name by calling the Performing Date Formatting and Currency Formatting -------------------------------------------------- -:app:`Pyramid` does not itself perform date and currency formatting -for different locales. However, :term:`Babel` can help you do this -via the :class:`babel.core.Locale` class. The `Babel documentation -for this class -<http://babel.edgewall.org/wiki/ApiDocs/babel.core#babel.core:Locale>`_ -provides minimal information about how to perform date and currency -related locale operations. See :ref:`installing_babel` for -information about how to install Babel. - -The :class:`babel.core.Locale` class requires a :term:`locale name` as -an argument to its constructor. You can use :app:`Pyramid` APIs to -obtain the locale name for a request to pass to the -:class:`babel.core.Locale` constructor; see -:ref:`obtaining_the_locale_name`. For example: +:app:`Pyramid` does not itself perform date and currency formatting for +different locales. However, :term:`Babel` can help you do this via the +:class:`babel.core.Locale` class. The `Babel documentation for this class +<http://babel.pocoo.org/en/latest/api/core.html#basic-interface>`_ provides +minimal information about how to perform date and currency related locale +operations. See :ref:`installing_babel` for information about how to install +Babel. + +The :class:`babel.core.Locale` class requires a :term:`locale name` as an +argument to its constructor. You can use :app:`Pyramid` APIs to obtain the +locale name for a request to pass to the :class:`babel.core.Locale` +constructor. See :ref:`obtaining_the_locale_name`. For example: .. code-block:: python :linenos: from babel.core import Locale - from pyramid.i18n import get_locale_name def aview(request): - locale_name = get_locale_name(request) + locale_name = request.locale_name locale = Locale(locale_name) .. index:: @@ -692,15 +613,14 @@ obtain the locale name for a request to pass to the Chameleon Template Support for Translation Strings -------------------------------------------------- -When a :term:`translation string` is used as the subject of textual -rendering by a :term:`Chameleon` template renderer, it will -automatically be translated to the requesting user's language if a -suitable translation exists. This is true of both the ZPT and text -variants of the Chameleon template renderers. +When a :term:`translation string` is used as the subject of textual rendering +by a :term:`Chameleon` template renderer, it will automatically be translated +to the requesting user's language if a suitable translation exists. This is +true of both the ZPT and text variants of the Chameleon template renderers. -For example, in a Chameleon ZPT template, the translation string -represented by "some_translation_string" in each example below will go -through translation before being rendered: +For example, in a Chameleon ZPT template, the translation string represented by +"some_translation_string" in each example below will go through translation +before being rendered: .. code-block:: xml :linenos: @@ -725,31 +645,46 @@ through translation before being rendered: .. XXX the last example above appears to not yet work as of Chameleon .. 1.2.3 -The features represented by attributes of the ``i18n`` namespace of -Chameleon will also consult the :app:`Pyramid` translations. -See -`http://chameleon.repoze.org/docs/latest/i18n.html#the-i18n-namespace -<http://chameleon.repoze.org/docs/latest/i18n.html#the-i18n-namespace>`_. +The features represented by attributes of the ``i18n`` namespace of Chameleon +will also consult the :app:`Pyramid` translations. See +http://chameleon.readthedocs.org/en/latest/reference.html#id50. .. note:: - Unlike when Chameleon is used outside of :app:`Pyramid`, when it - is used *within* :app:`Pyramid`, it does not support use of the - ``zope.i18n`` translation framework. Applications which use - :app:`Pyramid` should use the features documented in this - chapter rather than ``zope.i18n``. + Unlike when Chameleon is used outside of :app:`Pyramid`, when it is used + *within* :app:`Pyramid`, it does not support use of the ``zope.i18n`` + translation framework. Applications which use :app:`Pyramid` should use the + features documented in this chapter rather than ``zope.i18n``. -Third party :app:`Pyramid` template renderers might not provide -this support out of the box and may need special code to do an -equivalent. For those, you can always use the more manual translation -facility described in :ref:`performing_a_translation`. +Third party :app:`Pyramid` template renderers might not provide this support +out of the box and may need special code to do an equivalent. For those, you +can always use the more manual translation facility described in +:ref:`performing_a_translation`. -Mako Pyramid I18N Support +.. index:: + single: Mako i18n + +Mako Pyramid i18n Support ------------------------- -There exists a recipe within the :term:`Pyramid Cookbook` named "Mako -Internationalization" which explains how to add idiomatic I18N support to -:term:`Mako` templates. +There exists a recipe within the :term:`Pyramid Community Cookbook` named +:ref:`Mako Internationalization <cookbook:mako_i18n>` which explains how to add +idiomatic i18n support to :term:`Mako` templates. + + +.. index:: + single: Jinja2 i18n + +Jinja2 Pyramid i18n Support +--------------------------- + +The add-on `pyramid_jinja2 <https://github.com/Pylons/pyramid_jinja2>`_ +provides a scaffold with an example of how to use internationalization with +Jinja2 in Pyramid. See the documentation sections `Internalization (i18n) +<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#internalization-i18n>`_ +and `Paster Template I18N +<http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/#paster-template-i18n>`_. + .. index:: single: localization deployment settings @@ -760,83 +695,83 @@ Internationalization" which explains how to add idiomatic I18N support to Localization-Related Deployment Settings ---------------------------------------- -A :app:`Pyramid` application will have a ``default_locale_name`` -setting. This value represents the :term:`default locale name` used -when the :term:`locale negotiator` returns ``None``. Pass it to the -:mod:`~pyramid.config.Configurator` constructor at startup -time: +A :app:`Pyramid` application will have a ``pyramid.default_locale_name`` +setting. This value represents the :term:`default locale name` used when the +:term:`locale negotiator` returns ``None``. Pass it to the +:mod:`~pyramid.config.Configurator` constructor at startup time: .. code-block:: python :linenos: from pyramid.config import Configurator - config = Configurator(settings={'default_locale_name':'de'}) + config = Configurator(settings={'pyramid.default_locale_name':'de'}) -You may alternately supply a ``default_locale_name`` via an -application's Paster ``.ini`` file: +You may alternately supply a ``pyramid.default_locale_name`` via an +application's ``.ini`` file: .. code-block:: ini :linenos: [app:main] - use = egg:MyProject#app - reload_templates = true - debug_authorization = false - debug_notfound = false - default_locale_name = de + use = egg:MyProject + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.default_locale_name = de -If this value is not supplied via the Configurator constructor or via -a Paste config file, it will default to ``en``. +If this value is not supplied via the Configurator constructor or via a config +file, it will default to ``en``. -If this setting is supplied within the :app:`Pyramid` application -``.ini`` file, it will be available as a settings key: +If this setting is supplied within the :app:`Pyramid` application ``.ini`` +file, it will be available as a settings key: .. code-block:: python :linenos: from pyramid.threadlocal import get_current_registry settings = get_current_registry().settings - default_locale_name = settings['default_locale_name'] + default_locale_name = settings['pyramid.default_locale_name'] + +.. index:: + single: detecting languages "Detecting" Available Languages ------------------------------- -Other systems provide an API that returns the set of "available -languages" as indicated by the union of all languages in all -translation directories on disk at the time of the call to the API. +Other systems provide an API that returns the set of "available languages" as +indicated by the union of all languages in all translation directories on disk +at the time of the call to the API. -It is by design that :app:`Pyramid` doesn't supply such an API. -Instead, the application itself is responsible for knowing the "available -languages". The rationale is this: any particular application -deployment must always know which languages it should be translatable -to anyway, regardless of which translation files are on disk. +It is by design that :app:`Pyramid` doesn't supply such an API. Instead the +application itself is responsible for knowing the "available languages". The +rationale is this: any particular application deployment must always know which +languages it should be translatable to anyway, regardless of which translation +files are on disk. -Here's why: it's not a given that because translations exist in a -particular language within the registered set of translation -directories that this particular deployment wants to allow translation -to that language. For example, some translations may exist but they -may be incomplete or incorrect. Or there may be translations to a -language but not for all translation domains. +Here's why: it's not a given that because translations exist in a particular +language within the registered set of translation directories that this +particular deployment wants to allow translation to that language. For +example, some translations may exist but they may be incomplete or incorrect. +Or there may be translations to a language but not for all translation domains. Any nontrivial application deployment will always need to be able to -selectively choose to allow only some languages even if that set of -languages is smaller than all those detected within registered -translation directories. The easiest way to allow for this is to make -the application entirely responsible for knowing which languages are -allowed to be translated to instead of relying on the framework to -divine this information from translation directory file info. +selectively choose to allow only some languages even if that set of languages +is smaller than all those detected within registered translation directories. +The easiest way to allow for this is to make the application entirely +responsible for knowing which languages are allowed to be translated to instead +of relying on the framework to divine this information from translation +directory file info. -You can set up a system to allow a deployer to select available -languages based on convention by using the :mod:`pyramid.settings` -mechanism: +You can set up a system to allow a deployer to select available languages based +on convention by using the :mod:`pyramid.settings` mechanism. -Allow a deployer to modify your application's PasteDeploy .ini file: +Allow a deployer to modify your application's ``.ini`` file: .. code-block:: ini :linenos: [app:main] - use = egg:MyProject#app + use = egg:MyProject # ... available_languages = fr de en ru @@ -845,53 +780,60 @@ Then as a part of the code of a custom :term:`locale negotiator`: .. code-block:: python :linenos: - from pyramid.threadlocal import get_current_registry - settings = get_current_registry().settings - languages = settings['available_languages'].split() + from pyramid.settings import aslist -This is only a suggestion. You can create your own "available -languages" configuration scheme as necessary. + def my_locale_negotiator(request): + languages = aslist(request.registry.settings['available_languages']) + # ... + +This is only a suggestion. You can create your own "available languages" +configuration scheme as necessary. .. index:: pair: translation; activating pair: locale; negotiator single: translation directory +.. index:: + pair: activating; translation + .. _activating_translation: Activating Translation ---------------------- -By default, a :app:`Pyramid` application performs no translation. -To turn translation on, you must: +By default, a :app:`Pyramid` application performs no translation. To turn +translation on you must: - add at least one :term:`translation directory` to your application. - ensure that your application sets the :term:`locale name` correctly. +.. index:: + pair: translation directory; adding + .. _adding_a_translation_directory: Adding a Translation Directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:term:`gettext` is the underlying machinery behind the -:app:`Pyramid` translation machinery. A translation directory is a -directory organized to be useful to :term:`gettext`. A translation -directory usually includes a listing of language directories, each of -which itself includes an ``LC_MESSAGES`` directory. Each -``LC_MESSAGES`` directory should contain one or more ``.mo`` files. -Each ``.mo`` file represents a :term:`message catalog`, which is used -to provide translations to your application. +:term:`gettext` is the underlying machinery behind the :app:`Pyramid` +translation machinery. A translation directory is a directory organized to be +useful to :term:`gettext`. A translation directory usually includes a listing +of language directories, each of which itself includes an ``LC_MESSAGES`` +directory. Each ``LC_MESSAGES`` directory should contain one or more ``.mo`` +files. Each ``.mo`` file represents a :term:`message catalog`, which is used to +provide translations to your application. Adding a :term:`translation directory` registers all of its constituent -:term:`message catalog` files within your :app:`Pyramid` application to -be available to use for translation services. This includes all of the -``.mo`` files found within all ``LC_MESSAGES`` directories within each -locale directory in the translation directory. +:term:`message catalog` files within your :app:`Pyramid` application to be +available to use for translation services. This includes all of the ``.mo`` +files found within all ``LC_MESSAGES`` directories within each locale directory +in the translation directory. You can add a translation directory imperatively by using the -:meth:`pyramid.config.Configurator.add_translation_dirs` during -application startup. For example: +:meth:`pyramid.config.Configurator.add_translation_dirs` during application +startup. For example: .. code-block:: python :linenos: @@ -901,53 +843,56 @@ application startup. For example: 'another.application:locale/') A message catalog in a translation directory added via -:meth:`~pyramid.config.Configurator.add_translation_dirs` -will be merged into translations from a message catalog added earlier -if both translation directories contain translations for the same -locale and :term:`translation domain`. +:meth:`~pyramid.config.Configurator.add_translation_dirs` will be merged into +translations from a message catalog added earlier if both translation +directories contain translations for the same locale and :term:`translation +domain`. + +.. index:: + pair: setting; locale Setting the Locale ~~~~~~~~~~~~~~~~~~ -When the *default locale negotiator* (see -:ref:`default_locale_negotiator`) is in use, you can inform -:app:`Pyramid` of the current locale name by doing any of these -things before any translations need to be performed: +When the *default locale negotiator* (see :ref:`default_locale_negotiator`) is +in use, you can inform :app:`Pyramid` of the current locale name by doing any +of these things before any translations need to be performed: -- Set the ``_LOCALE_`` attribute of the request to a valid locale name - (usually directly within view code). E.g. ``request._LOCALE_ = - 'de'``. +- Set the ``_LOCALE_`` attribute of the request to a valid locale name (usually + directly within view code), e.g., ``request._LOCALE_ = 'de'``. -- Ensure that a valid locale name value is in the ``request.params`` - dictionary under the key named ``_LOCALE_``. This is usually the - result of passing a ``_LOCALE_`` value in the query string or in the - body of a form post associated with a request. For example, - visiting ``http://my.application?_LOCALE_=de``. +- Ensure that a valid locale name value is in the ``request.params`` dictionary + under the key named ``_LOCALE_``. This is usually the result of passing a + ``_LOCALE_`` value in the query string or in the body of a form post + associated with a request. For example, visiting + ``http://my.application?_LOCALE_=de``. - Ensure that a valid locale name value is in the ``request.cookies`` - dictionary under the key named ``_LOCALE_``. This is usually the - result of setting a ``_LOCALE_`` cookie in a prior response, - e.g. ``response.set_cookie('_LOCALE_', 'de')``. + dictionary under the key named ``_LOCALE_``. This is usually the result of + setting a ``_LOCALE_`` cookie in a prior response, e.g., + ``response.set_cookie('_LOCALE_', 'de')``. .. note:: If this locale negotiation scheme is inappropriate for a particular - application, you can configure a custom :term:`locale negotiator` - function into that application as required. See - :ref:`custom_locale_negotiator`. + application, you can configure a custom :term:`locale negotiator` function + into that application as required. See :ref:`custom_locale_negotiator`. + +.. index:: + single: locale negotiator .. _locale_negotiators: Locale Negotiators ------------------ -A :term:`locale negotiator` informs the operation of a -:term:`localizer` by telling it what :term:`locale name` is related to -a particular request. A locale negotiator is a bit of code which -accepts a request and which returns a :term:`locale name`. It is -consulted when :meth:`pyramid.i18n.Localizer.translate` or -:meth:`pyramid.i18n.Localizer.pluralize` is invoked. It is also -consulted when :func:`~pyramid.i18n.get_locale_name` or +A :term:`locale negotiator` informs the operation of a :term:`localizer` by +telling it what :term:`locale name` is related to a particular request. A +locale negotiator is a bit of code which accepts a request and which returns a +:term:`locale name`. It is consulted when +:meth:`pyramid.i18n.Localizer.translate` or +:meth:`pyramid.i18n.Localizer.pluralize` is invoked. It is also consulted when +:func:`~pyramid.request.Request.locale_name` is accessed or when :func:`~pyramid.i18n.negotiate_locale_name` is invoked. .. _default_locale_negotiator: @@ -955,63 +900,59 @@ consulted when :func:`~pyramid.i18n.get_locale_name` or The Default Locale Negotiator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Most applications can make use of the default locale negotiator, which -requires no additional coding or configuration. +Most applications can make use of the default locale negotiator, which requires +no additional coding or configuration. The default locale negotiator implementation named -:class:`~pyramid.i18n.default_locale_negotiator` uses the following -set of steps to dermine the locale name. +:class:`~pyramid.i18n.default_locale_negotiator` uses the following set of +steps to determine the locale name. -- First, the negotiator looks for the ``_LOCALE_`` attribute of the - request object (possibly set directly by view code or by a listener - for an :term:`event`). +- First the negotiator looks for the ``_LOCALE_`` attribute of the request + object (possibly set directly by view code or by a listener for an + :term:`event`). - Then it looks for the ``request.params['_LOCALE_']`` value. - Then it looks for the ``request.cookies['_LOCALE_']`` value. -- If no locale can be found via the request, it falls back to using - the :term:`default locale name` (see - :ref:`localization_deployment_settings`). +- If no locale can be found via the request, it falls back to using the + :term:`default locale name` (see :ref:`localization_deployment_settings`). -- Finally, if the default locale name is not explicitly set, it uses - the locale name ``en``. +- Finally if the default locale name is not explicitly set, it uses the locale + name ``en``. .. _custom_locale_negotiator: Using a Custom Locale Negotiator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Locale negotiation is sometimes policy-laden and complex. If the -(simple) default locale negotiation scheme described in -:ref:`activating_translation` is inappropriate for your application, -you may create and a special :term:`locale negotiator`. Subsequently -you may override the default locale negotiator by adding your newly -created locale negotiator to your application's configuration. +Locale negotiation is sometimes policy-laden and complex. If the (simple) +default locale negotiation scheme described in :ref:`activating_translation` is +inappropriate for your application, you may create a special :term:`locale +negotiator`. Subsequently you may override the default locale negotiator by +adding your newly created locale negotiator to your application's +configuration. -A locale negotiator is simply a callable which -accepts a request and returns a single :term:`locale name` or ``None`` -if no locale can be determined. +A locale negotiator is simply a callable which accepts a request and returns a +single :term:`locale name` or ``None`` if no locale can be determined. Here's an implementation of a simple locale negotiator: .. code-block:: python - :linenos: + :linenos: def my_locale_negotiator(request): locale_name = request.params.get('my_locale') return locale_name -If a locale negotiator returns ``None``, it signifies to -:app:`Pyramid` that the default application locale name should be -used. +If a locale negotiator returns ``None``, it signifies to :app:`Pyramid` that +the default application locale name should be used. You may add your newly created locale negotiator to your application's configuration by passing an object which can act as the negotiator (or a :term:`dotted Python name` referring to the object) as the -``locale_negotiator`` argument of the -:class:`~pyramid.config.Configurator` instance during application -startup. For example: +``locale_negotiator`` argument of the :class:`~pyramid.config.Configurator` +instance during application startup. For example: .. code-block:: python :linenos: @@ -1019,9 +960,8 @@ startup. For example: from pyramid.config import Configurator config = Configurator(locale_negotiator=my_locale_negotiator) -Alternately, use the -:meth:`pyramid.config.Configurator.set_locale_negotiator` -method. +Alternatively, use the +:meth:`pyramid.config.Configurator.set_locale_negotiator` method. For example: @@ -1031,4 +971,3 @@ For example: from pyramid.config import Configurator config = Configurator() config.set_locale_negotiator(my_locale_negotiator) - diff --git a/docs/narr/install.rst b/docs/narr/install.rst index fe8459c6f..3e5523262 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -1,256 +1,176 @@ .. _installing_chapter: Installing :app:`Pyramid` -============================ +========================= + +.. note:: + + This installation guide emphasizes the use of Python 3.4 and greater for + simplicity. + .. index:: single: install preparation -Before You Install ------------------- +Before You Install Pyramid +-------------------------- -You will need `Python <http://python.org>`_ version 2.4 or better to -run :app:`Pyramid`. +Install Python version 3.4 or greater for your operating system, and satisfy +the :ref:`requirements-for-installing-packages`, as described in +the following sections. .. sidebar:: Python Versions - As of this writing, :app:`Pyramid` has been tested under Python - 2.4.6, Python 2.5.4 and Python 2.6.2, and Python 2.7. To ensure - backwards compatibility, development of :app:`Pyramid` is - currently done primarily under Python 2.4 and Python 2.5. - :app:`Pyramid` does not run under any version of Python before - 2.4, and does not yet run under Python 3.X. + As of this writing, :app:`Pyramid` has been tested under Python 2.7, + Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. :app:`Pyramid` does + not run under any version of Python before 2.7. -:app:`Pyramid` is known to run on all popular Unix-like systems such as -Linux, MacOS X, and FreeBSD as well as on Windows platforms. It is also -known to run on Google's App Engine and :term:`Jython`. +:app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux, +Mac OS X, and FreeBSD, as well as on Windows platforms. It is also known to +run on :term:`PyPy` (1.9+). -:app:`Pyramid` installation does not require the compilation of any -C code, so you need only a Python interpreter that meets the -requirements mentioned. +:app:`Pyramid` installation does not require the compilation of any C code. +However, some :app:`Pyramid` dependencies may attempt to build binary +extensions from C code for performance speed ups. If a compiler or Python +headers are unavailable, the dependency will fall back to using pure Python +instead. -If You Don't Yet Have A Python Interpreter (UNIX) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: -If your system doesn't have a Python interpreter, and you're on UNIX, -you can either install Python using your operating system's package -manager *or* you can install Python from source fairly easily on any -UNIX system that has development tools. + If you see any warnings or errors related to failing to compile the binary + extensions, in most cases you may safely ignore those errors. If you wish to + use the binary extensions, please verify that you have a functioning + compiler and the Python header files installed for your operating system. -Package Manager Method -++++++++++++++++++++++ -You can use your system's "package manager" to install Python. Every -system's package manager is slightly different, but the "flavor" of -them is usually the same. - -For example, on an Ubuntu Linux system, to use the system package -manager to install a Python 2.6 interpreter, use the following -command: - -.. code-block:: text +.. _for-mac-os-x-users: - $ sudo apt-get install python2.6-dev +For Mac OS X Users +~~~~~~~~~~~~~~~~~~ -Once these steps are performed, the Python interpreter will usually be -invokable via ``python2.6`` from a shell prompt. +Python comes pre-installed on Mac OS X, but due to Apple's release cycle, it is +often out of date. Unless you have a need for a specific earlier version, it is +recommended to install the latest 3.x version of Python. -Source Compile Method -+++++++++++++++++++++ +You can install the latest verion of Python for Mac OS X from the binaries on +`python.org <https://www.python.org/downloads/mac-osx/>`_. -It's useful to use a Python interpreter that *isn't* the "system" -Python interpreter to develop your software. The authors of -:app:`Pyramid` tend not to use the system Python for development -purposes; always a self-compiled one. Compiling Python is usually -easy, and often the "system" Python is compiled with options that -aren't optimal for web development. - -To compile software on your UNIX system, typically you need -development tools. Often these can be installed via the package -manager. For example, this works to do so on an Ubuntu Linux system: +Alternatively, you can use the `homebrew <http://brew.sh/>`_ package manager. .. code-block:: text - $ sudo apt-get install build-essential + # for python 3.x + $ brew install python3 -On Mac OS X, installing `XCode -<http://developer.apple.com/tools/xcode/>`_ has much the same effect. +If you use an installer for your Python, then you can skip to the section +:ref:`installing_unix`. -Once you've got development tools installed on your system, On the -same system, to install a Python 2.6 interpreter from *source*, use -the following commands: -.. code-block:: text +.. _if-you-don-t-yet-have-a-python-interpreter-unix: - [chrism@vitaminf ~]$ cd ~ - [chrism@vitaminf ~]$ mkdir tmp - [chrism@vitaminf ~]$ mkdir opt - [chrism@vitaminf ~]$ cd tmp - [chrism@vitaminf tmp]$ wget \ - http://www.python.org/ftp/python/2.6.4/Python-2.6.4.tgz - [chrism@vitaminf tmp]$ tar xvzf Python-2.6.4.tgz - [chrism@vitaminf tmp]$ cd Python-2.6.4 - [chrism@vitaminf Python-2.6.4]$ ./configure \ - --prefix=$HOME/opt/Python-2.6.4 - [chrism@vitaminf Python-2.6.4]$ make; make install - -Once these steps are performed, the Python interpreter will be -invokable via ``$HOME/opt/Python-2.6.4/bin/python`` from a shell -prompt. - -If You Don't Yet Have A Python Interpreter (Windows) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If You Don't Yet Have a Python Interpreter (UNIX) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If your Windows system doesn't have a Python interpreter, you'll need -to install it by downloading a Python 2.6-series interpreter -executable from `python.org's download section -<http://python.org/download/>`_ (the files labeled "Windows -Installer"). Once you've downloaded it, double click on the -executable and accept the defaults during the installation process. -You may also need to download and install the `Python for Windows -extensions <http://sourceforge.net/projects/pywin32/files/>`_. +If your system doesn't have a Python interpreter, and you're on UNIX, you can +either install Python using your operating system's package manager *or* you +can install Python from source fairly easily on any UNIX system that has +development tools. -.. warning:: +.. seealso:: See the official Python documentation :ref:`Using Python on Unix + platforms <python:using-on-unix>` for full details. - After you install Python on Windows, you may need to add the - ``C:\Python26`` directory to your environment's ``Path`` in order - to make it possible to invoke Python from a command prompt by - typing ``python``. To do so, right click ``My Computer``, select - ``Properties`` --> ``Advanced Tab`` --> ``Environment Variables`` - and add that directory to the end of the ``Path`` environment - variable. .. index:: - single: installing on UNIX + pair: install; Python (from package, Windows) -.. _installing_unix: - -Installing :app:`Pyramid` on a UNIX System ---------------------------------------------- - -It is best practice to install :app:`Pyramid` into a "virtual" -Python environment in order to obtain isolation from any "system" -packages you've got installed in your Python version. This can be -done by using the :term:`virtualenv` package. Using a virtualenv will -also prevent :app:`Pyramid` from globally installing versions of -packages that are not compatible with your system Python. +.. _if-you-don-t-yet-have-a-python-interpreter-windows: -To set up a virtualenv in which to install :app:`Pyramid`, first -ensure that :term:`setuptools` is installed. Invoke ``import -setuptools`` within the Python interpreter you'd like to run -:app:`Pyramid` under: +If You Don't Yet Have a Python Interpreter (Windows) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: text +If your Windows system doesn't have a Python interpreter, you'll need to +install it by downloading a Python 3.x-series interpreter executable from +`python.org's download section <http://python.org/download/>`_ (the files +labeled "Windows Installer"). Once you've downloaded it, double click on the +executable and accept the defaults during the installation process. You may +also need to download and install the Python for Windows extensions. - [chrism@vitaminf pyramid]$ python - Python 2.4.5 (#1, Aug 29 2008, 12:27:37) - [GCC 4.0.1 (Apple Inc. build 5465)] on darwin - Type "help", "copyright", "credits" or "license" for more information. - >>> import setuptools - -If running ``import setuptools`` does not raise an ``ImportError``, it -means that setuptools is already installed into your Python -interpreter. If ``import setuptools`` fails, you will need to install -setuptools manually. Note that above we're using a Python 2.4-series -interpreter on Mac OS X; your output may differ if you're using a -later Python version or a different platform. - -If you are using a "system" Python (one installed by your OS -distributor or a 3rd-party packager such as Fink or MacPorts), you can -usually install the setuptools package by using your system's package -manager. If you cannot do this, or if you're using a self-installed -version of Python, you will need to install setuptools "by hand". -Installing setuptools "by hand" is always a reasonable thing to do, -even if your package manager already has a pre-chewed version of -setuptools for installation. - -To install setuptools by hand, first download `ez_setup.py -<http://peak.telecommunity.com/dist/ez_setup.py>`_ then invoke it -using the Python interpreter into which you want to install -setuptools. +.. seealso:: See the official Python documentation :ref:`Using Python on + Windows <python:using-on-windows>` for full details. -.. code-block:: text +.. seealso:: Download and install the `Python for Windows extensions + <http://sourceforge.net/projects/pywin32/files/pywin32/>`_. Carefully read + the README.txt file at the end of the list of builds, and follow its + directions. Make sure you get the proper 32- or 64-bit build and Python + version. - $ python ez_setup.py +.. warning:: -Once this command is invoked, setuptools should be installed on your -system. If the command fails due to permission errors, you may need -to be the administrative user on your system to successfully invoke -the script. To remediate this, you may need to do: + After you install Python on Windows, you may need to add the ``C:\Python3x`` + directory to your environment's ``Path``, where ``x`` is the minor version + of installed Python, in order to make it possible to invoke Python from a + command prompt by typing ``python``. To do so, right click ``My Computer``, + select ``Properties`` --> ``Advanced Tab`` --> ``Environment Variables`` and + add that directory to the end of the ``Path`` environment variable. -.. code-block:: text + .. seealso:: See `Configuring Python (on Windows) + <https://docs.python.org/3/using/windows.html#configuring-python>`_ for + full details. - $ sudo python ez_setup.py .. index:: - single: virtualenv + single: requirements for installing packages -Installing the ``virtualenv`` Package -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _requirements-for-installing-packages: -Once you've got setuptools installed, you should install the -:term:`virtualenv` package. To install the :term:`virtualenv` package -into your setuptools-enabled Python interpreter, use the -``easy_install`` command. +Requirements for Installing Packages +------------------------------------ -.. code-block:: text +Use :term:`pip` for installing packages and ``python3 -m venv env`` for +creating a virtual environment. A virtual environment is a semi-isolated Python +environment that allows packages to be installed for use by a particular +application, rather than being installed system wide. - $ easy_install virtualenv +.. seealso:: See the Python Packaging Authority's (PyPA) documention + `Requirements for Installing Packages + <https://packaging.python.org/en/latest/installing/#requirements-for-installing-packages>`_ + for full details. -This command should succeed, and tell you that the virtualenv package -is now installed. If it fails due to permission errors, you may need -to install it as your system's administrative user. For example: - -.. code-block:: text - - $ sudo easy_install virtualenv .. index:: - single: virtualenv + single: installing on UNIX + single: installing on Mac OS X -Creating the Virtual Python Environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _installing_unix: -Once the :term:`virtualenv` package is installed in your Python, you -can then create a virtual environment. To do so, invoke the -following: +Installing :app:`Pyramid` on a UNIX System +------------------------------------------ -.. code-block:: text +After installing Python as described previously in :ref:`for-mac-os-x-users` or +:ref:`if-you-don-t-yet-have-a-python-interpreter-unix`, and satisfying the +:ref:`requirements-for-installing-packages`, you can now install Pyramid. - $ virtualenv --no-site-packages env - New python executable in env/bin/python - Installing setuptools.............done. +#. Make a :term:`virtual environment` workspace: -.. warning:: Using ``--no-site-packages`` when generating your - virtualenv is *very important*. This flag provides the necessary - isolation for running the set of packages required by - :app:`Pyramid`. If you do not specify ``--no-site-packages``, - it's possible that :app:`Pyramid` will not install properly into - the virtualenv, or, even if it does, may not run properly, - depending on the packages you've already got installed into your - Python's "main" site-packages dir. + .. code-block:: bash -.. warning:: If you're on UNIX, *do not* use ``sudo`` to run the - ``virtualenv`` script. It's perfectly acceptable (and desirable) - to create a virtualenv as a normal user. + $ export VENV=~/env + $ python3 -m venv $VENV -You should perform any following commands that mention a "bin" -directory from within the ``env`` virtualenv dir. + You can either follow the use of the environment variable ``$VENV``, or + replace it with the root directory of the virtual environment. If you choose + the former approach, ensure that ``$VENV`` is an absolute path. In the + latter case, the ``export`` command can be skipped. -Installing :app:`Pyramid` Into the Virtual Python Environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#. (Optional) Consider using ``$VENV/bin/activate`` to make your shell + environment wired to use the virtual environment. -After you've got your ``env`` virtualenv installed, you may install -:app:`Pyramid` itself using the following commands from within the -virtualenv (``env``) directory you created in the last step: +#. Use ``pip`` to get :app:`Pyramid` and its direct dependencies installed: -.. code-block:: text + .. parsed-literal:: - $ cd env - $ bin/easy_install pyramid + $ $VENV/bin/pip install "pyramid==\ |release|\ " -The ``easy_install`` command will take longer than the previous ones to -complete, as it downloads and installs a number of dependencies. .. index:: single: installing on Windows @@ -258,86 +178,40 @@ complete, as it downloads and installs a number of dependencies. .. _installing_windows: Installing :app:`Pyramid` on a Windows System -------------------------------------------------- - -#. Install, or find `Python 2.6 - <http://python.org/download/releases/2.6.4/>`_ for your system. - -#. Install the `Python for Windows extensions - <http://sourceforge.net/projects/pywin32/files/>`_. Make sure to - pick the right download for Python 2.6 and install it using the - same Python installation from the previous step. - -#. Install latest :term:`setuptools` distribution into the Python you - obtained/installed/found in the step above: download `ez_setup.py - <http://peak.telecommunity.com/dist/ez_setup.py>`_ and run it using - the ``python`` interpreter of your Python 2.6 installation using a - command prompt: - - .. code-block:: text - - c:\> c:\Python26\python ez_setup.py - -#. Use that Python's `bin/easy_install` to install `virtualenv`: - - .. code-block:: text - - c:\> c:\Python26\Scripts\easy_install virtualenv - -#. Use that Python's virtualenv to make a workspace: - - .. code-block:: text - - c:\> c:\Python26\Scripts\virtualenv --no-site-packages env - -#. Switch to the ``env`` directory: - - .. code-block:: text - - c:\> cd env +--------------------------------------------- -#. (Optional) Consider using ``Scripts\activate.bat`` to make your shell - environment wired to use the virtualenv. +After installing Python as described previously in +:ref:`if-you-don-t-yet-have-a-python-interpreter-windows`, and satisfying the +:ref:`requirements-for-installing-packages`, you can now install Pyramid. -#. Use ``easy_install`` pointed at the "current" index to get - :app:`Pyramid` and its direct dependencies installed: +#. Make a :term:`virtual environment` workspace: - .. code-block:: text + .. code-block:: doscon - c:\env> Scripts\easy_install pyramid + c:\> set VENV=c:\env + # replace "x" with your minor version of Python 3 + c:\> c:\Python3x\Scripts\python3 -m venv %VENV% -.. index:: - single: installing on Google App Engine + You can either follow the use of the environment variable ``%VENV%``, or + replace it with the root directory of the virtual environment. If you choose + the former approach, ensure that ``%VENV%`` is an absolute path. In the + latter case, the ``set`` command can be skipped. -Installing :app:`Pyramid` on Google App Engine -------------------------------------------------- +#. (Optional) Consider using ``%VENV%\Scripts\activate.bat`` to make your shell + environment wired to use the virtual environment. -:ref:`appengine_tutorial` documents the steps required to install a -:app:`Pyramid` application on Google App Engine. +#. Use ``pip`` to get :app:`Pyramid` and its direct dependencies installed: -Installing :app:`Pyramid` on Jython --------------------------------------- + .. parsed-literal:: -:app:`Pyramid` is known to work under :term:`Jython` version 2.5.1. -Install :term:`Jython`, and then follow the installation steps for -:app:`Pyramid` on your platform described in one of the sections -entitled :ref:`installing_unix` or :ref:`installing_windows` above, -replacing the ``python`` command with ``jython`` as necessary. The -steps are exactly the same except you should use the ``jython`` -command name instead of the ``python`` command name. + c:\\env> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ " -One caveat exists to using :app:`Pyramid` under Jython: the :term:`Chameleon` -templating engine does not work on Jython. However, the :term:`Mako` -templating system, which is also included with Pyramid, does work under -Jython; use it instead. What Gets Installed ------------------- -When you ``easy_install`` :app:`Pyramid`, various Zope libraries, -various Chameleon libraries, WebOb, Paste, PasteScript, and -PasteDeploy libraries are installed. - -Additionally, as chronicled in :ref:`project_narr`, scaffolds will be registered, -which make it easy to start a new :app:`Pyramid` project. +When you install :app:`Pyramid`, various libraries such as WebOb, PasteDeploy, +and others are installed. +Additionally, as chronicled in :ref:`project_narr`, scaffolds will be +registered, which make it easy to start a new :app:`Pyramid` project. diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index a0b682e25..24c9f6b93 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -7,83 +7,903 @@ single: framework :app:`Pyramid` Introduction -============================== +=========================== :app:`Pyramid` is a general, open source, Python web application development -*framework*. Its primary goal is to make it easier for a developer to create -web applications. The type of application being created could be a -spreadsheet, a corporate intranet, or a social networking platform; Pyramid's -generality enables it to be used to build an unconstrained variety of web -applications. +*framework*. Its primary goal is to make it easier for a Python developer to +create web applications. .. sidebar:: Frameworks vs. Libraries - A *framework* differs from a *library* in one very important way: - library code is always *called* by code that you write, while a - framework always *calls* code that you write. Using a set of - libraries to create an application is usually easier than using a - framework initially, because you can choose to cede control to - library code you have not authored very selectively. But when you - use a framework, you are required to cede a greater portion of - control to code you have not authored: code that resides in the - framework itself. You needn't use a framework at all to create a - web application using Python. A rich set of libraries already - exists for the platform. In practice, however, using a framework - to create an application is often more practical than rolling your - own via a set of libraries if the framework provides a set of - facilities that fits your application requirements. + A *framework* differs from a *library* in one very important way: library + code is always *called* by code that you write, while a framework always + *calls* code that you write. Using a set of libraries to create an + application is usually easier than using a framework initially, because you + can choose to cede control to library code you have not authored very + selectively. But when you use a framework, you are required to cede a + greater portion of control to code you have not authored: code that resides + in the framework itself. You needn't use a framework at all to create a web + application using Python. A rich set of libraries already exists for the + platform. In practice, however, using a framework to create an application + is often more practical than rolling your own via a set of libraries if the + framework provides a set of facilities that fits your application + requirements. -The first release of Pyramid's predecessor (named :mod:`repoze.bfg`) was made -in July of 2008. We have worked hard to ensure that Pyramid continues to -follow the design and engineering principles that we consider to be the core -characteristics of a successful framework: +Pyramid attempts to follow these design and engineering principles: Simplicity - :app:`Pyramid` takes a *"pay only for what you eat"* approach. This means - that you can get results even if you have only a partial understanding of - :app:`Pyramid`. It doesn’t force you to use any particular technology to - produce an application, and we try to keep the core set of concepts that - you need to understand to a minimum. + :app:`Pyramid` takes a *"pay only for what you eat"* approach. You can get + results even if you have only a partial understanding of :app:`Pyramid`. It + doesn't force you to use any particular technology to produce an application, + and we try to keep the core set of concepts that you need to understand to a + minimum. Minimalism - :app:`Pyramid` concentrates on providing fast, high-quality solutions to - the fundamental problems of creating a web application: the mapping of URLs - to code, templating, security and serving static assets. We consider these - to be the core activities that are common to nearly all web applications. + :app:`Pyramid` tries to solve only the fundamental problems of creating a web + application: the mapping of URLs to code, templating, security, and serving + static assets. We consider these to be the core activities that are common to + nearly all web applications. Documentation - Pyramid's minimalism means that it is relatively easy for us to maintain - extensive and up-to-date documentation. It is our goal that no aspect of - Pyramid remains undocumented. + Pyramid's minimalism means that it is easier for us to maintain complete and + up-to-date documentation. It is our goal that no aspect of Pyramid is + undocumented. Speed :app:`Pyramid` is designed to provide noticeably fast execution for common - tasks such as templating and simple response generation. Although the - “hardware is cheap” mantra may appear to offer a ready solution to speed - problems, the limits of this approach become painfully evident when one - finds him or herself responsible for managing a great many machines. + tasks such as templating and simple response generation. Reliability :app:`Pyramid` is developed conservatively and tested exhaustively. Where - Pyramid source code is concerned, our motto is: "If it ain’t tested, it’s - broke". Every release of Pyramid has 100% statement coverage via unit - tests. + Pyramid source code is concerned, our motto is: "If it ain't tested, it's + broke". Openness - As with Python, the Pyramid software is distributed under a `permissive - open source license <http://repoze.org/license.html>`_. + As with Python, the Pyramid software is distributed under a `permissive open + source license <http://repoze.org/license.html>`_. + +.. _what_makes_pyramid_unique: + +What makes Pyramid unique +------------------------- + +Understandably, people don't usually want to hear about squishy engineering +principles; they want to hear about concrete stuff that solves their problems. +With that in mind, what would make someone want to use Pyramid instead of one +of the many other web frameworks available today? What makes Pyramid unique? + +This is a hard question to answer because there are lots of excellent choices, +and it's actually quite hard to make a wrong choice, particularly in the Python +web framework market. But one reasonable answer is this: you can write very +small applications in Pyramid without needing to know a lot. "What?" you say. +"That can't possibly be a unique feature. Lots of other web frameworks let you +do that!" Well, you're right. But unlike many other systems, you can also +write very large applications in Pyramid if you learn a little more about it. +Pyramid will allow you to become productive quickly, and will grow with you. It +won't hold you back when your application is small, and it won't get in your +way when your application becomes large. "Well that's fine," you say. "Lots of +other frameworks let me write large apps, too." Absolutely. But other Python +web frameworks don't seamlessly let you do both. They seem to fall into two +non-overlapping categories: frameworks for "small apps" and frameworks for "big +apps". The "small app" frameworks typically sacrifice "big app" features, and +vice versa. + +We don't think it's a universally reasonable suggestion to write "small apps" +in a "small framework" and "big apps" in a "big framework". You can't really +know to what size every application will eventually grow. We don't really want +to have to rewrite a previously small application in another framework when it +gets "too big". We believe the current binary distinction between frameworks +for small and large applications is just false. A well-designed framework +should be able to be good at both. Pyramid strives to be that kind of +framework. + +To this end, Pyramid provides a set of features that combined are unique +amongst Python web frameworks. Lots of other frameworks contain some +combination of these features. Pyramid of course actually stole many of them +from those other frameworks. But Pyramid is the only one that has all of them +in one place, documented appropriately, and useful *à la carte* without +necessarily paying for the entire banquet. These are detailed below. + +Single-file applications +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can write a Pyramid application that lives entirely in one Python file, not +unlike existing Python microframeworks. This is beneficial for one-off +prototyping, bug reproduction, and very small applications. These applications +are easy to understand because all the information about the application lives +in a single place, and you can deploy them without needing to understand much +about Python distributions and packaging. Pyramid isn't really marketed as a +microframework, but it allows you to do almost everything that frameworks that +are marketed as "micro" offer in very similar ways. + +.. literalinclude:: helloworld.py + +.. seealso:: + + See also :ref:`firstapp_chapter`. + +Decorator-based configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you like the idea of framework configuration statements living next to the +code it configures, so you don't have to constantly switch between files to +refer to framework configuration when adding new code, you can use Pyramid +decorators to localize the configuration. For example: + +.. code-block:: python + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='fred') + def fred_view(request): + return Response('fred') + +However, unlike some other systems, using decorators for Pyramid configuration +does not make your application difficult to extend, test, or reuse. The +:class:`~pyramid.view.view_config` decorator, for example, does not actually +*change* the input or output of the function it decorates, so testing it is a +"WYSIWYG" operation. You don't need to understand the framework to test your +own code. You just behave as if the decorator is not there. You can also +instruct Pyramid to ignore some decorators, or use completely imperative +configuration instead of decorators to add views. Pyramid decorators are inert +instead of eager. You detect and activate them with a :term:`scan`. + +Example: :ref:`mapping_views_using_a_decorator_section`. + +URL generation +~~~~~~~~~~~~~~ + +Pyramid is capable of generating URLs for resources, routes, and static assets. +Its URL generation APIs are easy to use and flexible. If you use Pyramid's +various APIs for generating URLs, you can change your configuration around +arbitrarily without fear of breaking a link on one of your web pages. + +Example: :ref:`generating_route_urls`. + +Static file serving +~~~~~~~~~~~~~~~~~~~ + +Pyramid is perfectly willing to serve static files itself. It won't make you +use some external web server to do that. You can even serve more than one set +of static files in a single Pyramid web application (e.g., ``/static`` and +``/static2``). You can optionally place your files on an external web server +and ask Pyramid to help you generate URLs to those files. This let's you use +Pyramid's internal file serving while doing development, and a faster static +file server in production, without changing any code. + +Example: :ref:`static_assets_section`. + +Fully interactive development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing a Pyramid application, several interactive features are +available. Pyramid can automatically utilize changed templates when rendering +pages and automatically restart the application to incorporate changed Python +code. Plain old ``print()`` calls used for debugging can display to a console. + +Pyramid's debug toolbar comes activated when you use a Pyramid scaffold to +render a project. This toolbar overlays your application in the browser, and +allows you access to framework data, such as the routes configured, the last +renderings performed, the current set of packages installed, SQLAlchemy queries +run, logging data, and various other facts. When an exception occurs, you can +use its interactive debugger to poke around right in your browser to try to +determine the cause of the exception. It's handy. + +Example: :ref:`debug_toolbar`. + +Debugging settings +~~~~~~~~~~~~~~~~~~ + +Pyramid has debugging settings that allow you to print Pyramid runtime +information to the console when things aren't behaving as you're expecting. For +example, you can turn on ``debug_notfound``, which prints an informative +message to the console every time a URL does not match any view. You can turn +on ``debug_authorization``, which lets you know why a view execution was +allowed or denied by printing a message to the console. These features are +useful for those WTF moments. + +There are also a number of commands that you can invoke within a Pyramid +environment that allow you to introspect the configuration of your system. +``proutes`` shows all configured routes for an application in the order they'll +be evaluated for matching. ``pviews`` shows all configured views for any given +URL. These are also WTF-crushers in some circumstances. + +Examples: :ref:`debug_authorization_section` and :ref:`command_line_chapter`. + +Add-ons +~~~~~~~ + +Pyramid has an extensive set of add-ons held to the same quality standards as +the Pyramid core itself. Add-ons are packages which provide functionality that +the Pyramid core doesn't. Add-on packages already exist which let you easily +send email, let you use the Jinja2 templating system, let you use XML-RPC or +JSON-RPC, let you integrate with jQuery Mobile, etc. + +Examples: +http://docs.pylonsproject.org/en/latest/docs/pyramid.html#pyramid-add-on-documentation + +Class-based and function-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid has a structured, unified concept of a :term:`view callable`. View +callables can be functions, methods of classes, or even instances. When you +add a new view callable, you can choose to make it a function or a method of a +class. In either case Pyramid treats it largely the same way. You can change +your mind later and move code between methods of classes and functions. A +collection of similar view callables can be attached to a single class as +methods, if that floats your boat, and they can share initialization code as +necessary. All kinds of views are easy to understand and use, and operate +similarly. There is no phony distinction between them. They can be used for +the same purposes. + +Here's a view callable defined as a function: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + @view_config(route_name='aview') + def aview(request): + return Response('one') + +Here's a few views defined as methods of a class instead: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + class AView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='view_one') + def view_one(self): + return Response('one') + + @view_config(route_name='view_two') + def view_two(self): + return Response('two') + +.. seealso:: + + See also :ref:`view_config_placement`. + +.. _intro_asset_specs: + +Asset specifications +~~~~~~~~~~~~~~~~~~~~ + +Asset specifications are strings that contain both a Python package name and a +file or directory name, e.g., ``MyPackage:static/index.html``. Use of these +specifications is omnipresent in Pyramid. An asset specification can refer to +a template, a translation directory, or any other package-bound static +resource. This makes a system built on Pyramid extensible because you don't +have to rely on globals ("*the* static directory") or lookup schemes ("*the* +ordered set of template directories") to address your files. You can move +files around as necessary, and include other packages that may not share your +system's templates or static files without encountering conflicts. + +Because asset specifications are used heavily in Pyramid, we've also provided a +way to allow users to override assets. Say you love a system that someone else +has created with Pyramid but you just need to change "that one template" to +make it all better. No need to fork the application. Just override the asset +specification for that template with your own inside a wrapper, and you're good +to go. + +Examples: :ref:`asset_specifications` and :ref:`overriding_assets_section`. + +Extensible templating +~~~~~~~~~~~~~~~~~~~~~ + +Pyramid has a structured API that allows for pluggability of "renderers". +Templating systems such as Mako, Genshi, Chameleon, and Jinja2 can be treated +as renderers. Renderer bindings for all of these templating systems already +exist for use in Pyramid. But if you'd rather use another, it's not a big +deal. Just copy the code from an existing renderer package, and plug in your +favorite templating system. You'll then be able to use that templating system +from within Pyramid just as you'd use one of the "built-in" templating systems. + +Pyramid does not make you use a single templating system exclusively. You can +use multiple templating systems, even in the same project. + +Example: :ref:`templates_used_directly`. + +Rendered views can return dictionaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use a :term:`renderer`, you don't have to return a special kind of +"webby" ``Response`` object from a view. Instead you can return a dictionary, +and Pyramid will take care of converting that dictionary to a Response using a +template on your behalf. This makes the view easier to test, because you don't +have to parse HTML in your tests. Instead just make an assertion that the view +returns "the right stuff" in the dictionary. You can write "real" unit tests +instead of functionally testing all of your views. .. index:: - single: Pylons - single: Agendaless Consulting - single: repoze namespace package + pair: renderer; explicitly calling + pair: view renderer; explictly calling + +.. _example_render_to_response_call: + +For example, instead of returning a ``Response`` object from a +``render_to_response`` call: + +.. code-block:: python + :linenos: + + from pyramid.renderers import render_to_response + + def myview(request): + return render_to_response('myapp:templates/mytemplate.pt', {'a':1}, + request=request) + +You can return a Python dictionary: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='myapp:templates/mytemplate.pt') + def myview(request): + return {'a':1} + +When this view callable is called by Pyramid, the ``{'a':1}`` dictionary will +be rendered to a response on your behalf. The string passed as ``renderer=`` +above is an :term:`asset specification`. It is in the form +``packagename:directoryname/filename.ext``. In this case, it refers to the +``mytemplate.pt`` file in the ``templates`` directory within the ``myapp`` +Python package. Asset specifications are omnipresent in Pyramid. See +:ref:`intro_asset_specs` for more information. + +Example: :ref:`renderers_chapter`. + +Event system +~~~~~~~~~~~~ + +Pyramid emits *events* during its request processing lifecycle. You can +subscribe any number of listeners to these events. For example, to be notified +of a new request, you can subscribe to the ``NewRequest`` event. To be +notified that a template is about to be rendered, you can subscribe to the +``BeforeRender`` event, and so forth. Using an event publishing system as a +framework notification feature instead of hardcoded hook points tends to make +systems based on that framework less brittle. + +You can also use Pyramid's event system to send your *own* events. For +example, if you'd like to create a system that is itself a framework, and may +want to notify subscribers that a document has just been indexed, you can +create your own event type (``DocumentIndexed`` perhaps) and send the event via +Pyramid. Users of this framework can then subscribe to your event like they'd +subscribe to the events that are normally sent by Pyramid itself. + +Example: :ref:`events_chapter` and :ref:`event_types`. + +Built-in internationalization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid ships with internationalization-related features in its core: +localization, pluralization, and creating message catalogs from source files +and templates. Pyramid allows for a plurality of message catalogs via the use +of translation domains. You can create a system that has its own translations +without conflict with other translations in other domains. + +Example: :ref:`i18n_chapter`. + +HTTP caching +~~~~~~~~~~~~ + +Pyramid provides an easy way to associate views with HTTP caching policies. You +can just tell Pyramid to configure your view with an ``http_cache`` statement, +and it will take care of the rest:: + + @view_config(http_cache=3600) # 60 minutes + def myview(request): .... + +Pyramid will add appropriate ``Cache-Control`` and ``Expires`` headers to +responses generated when this view is invoked. + +See the :meth:`~pyramid.config.Configurator.add_view` method's ``http_cache`` +documentation for more information. + +Sessions +~~~~~~~~ + +Pyramid has built-in HTTP sessioning. This allows you to associate data with +otherwise anonymous users between requests. Lots of systems do this. But +Pyramid also allows you to plug in your own sessioning system by creating some +code that adheres to a documented interface. Currently there is a binding +package for the third-party Redis sessioning system that does exactly this. But +if you have a specialized need (perhaps you want to store your session data in +MongoDB), you can. You can even switch between implementations without +changing your application code. + +Example: :ref:`sessions_chapter`. + +Speed +~~~~~ + +The Pyramid core is, as far as we can tell, at least marginally faster than any +other existing Python web framework. It has been engineered from the ground up +for speed. It only does as much work as absolutely necessary when you ask it +to get a job done. Extraneous function calls and suboptimal algorithms in its +core codepaths are avoided. It is feasible to get, for example, between 3500 +and 4000 requests per second from a simple Pyramid view on commodity dual-core +laptop hardware and an appropriate WSGI server (mod_wsgi or gunicorn). In any +case, performance statistics are largely useless without requirements and +goals, but if you need speed, Pyramid will almost certainly never be your +application's bottleneck; at least no more than Python will be a bottleneck. + +Example: http://blog.curiasolutions.com/pages/the-great-web-framework-shootout.html + +Exception views +~~~~~~~~~~~~~~~ + +Exceptions happen. Rather than deal with exceptions that might present +themselves to a user in production in an ad-hoc way, Pyramid allows you to +register an :term:`exception view`. Exception views are like regular Pyramid +views, but they're only invoked when an exception "bubbles up" to Pyramid +itself. For example, you might register an exception view for the +:exc:`Exception` exception, which will catch *all* exceptions, and present a +pretty "well, this is embarrassing" page. Or you might choose to register an +exception view for only specific kinds of application-specific exceptions, such +as an exception that happens when a file is not found, or an exception that +happens when an action cannot be performed because the user doesn't have +permission to do something. In the former case, you can show a pretty "Not +Found" page; in the latter case you might show a login form. + +Example: :ref:`exception_views`. + +No singletons +~~~~~~~~~~~~~ + +Pyramid is written in such a way that it requires your application to have +exactly zero "singleton" data structures. Or put another way, Pyramid doesn't +require you to construct any "mutable globals". Or put even another different +way, an import of a Pyramid application needn't have any "import-time side +effects". This is esoteric-sounding, but if you've ever tried to cope with +parameterizing a Django ``settings.py`` file for multiple installations of the +same application, or if you've ever needed to monkey-patch some framework +fixture so that it behaves properly for your use case, or if you've ever wanted +to deploy your system using an asynchronous server, you'll end up appreciating +this feature. It just won't be a problem. You can even run multiple copies of +a similar but not identically configured Pyramid application within the same +Python process. This is good for shared hosting environments, where RAM is at +a premium. + +View predicates and many views per route +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike many other systems, Pyramid allows you to associate more than one view +per route. For example, you can create a route with the pattern ``/items`` and +when the route is matched, you can shuffle off the request to one view if the +request method is GET, another view if the request method is POST, etc. A +system known as "view predicates" allows for this. Request method matching is +the most basic thing you can do with a view predicate. You can also associate +views with other request parameters, such as the elements in the query string, +the Accept header, whether the request is an XHR request or not, and lots of +other things. This feature allows you to keep your individual views clean. +They won't need much conditional logic, so they'll be easier to test. + +Example: :ref:`view_configuration_parameters`. + +Transaction management +~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid's :term:`scaffold` system renders projects that include a *transaction +management* system, stolen from Zope. When you use this transaction management +system, you cease being responsible for committing your data anymore. Instead +Pyramid takes care of committing: it commits at the end of a request or aborts +if there's an exception. Why is that a good thing? Having a centralized place +for transaction management is a great thing. If, instead of managing your +transactions in a centralized place, you sprinkle ``session.commit`` calls in +your application logic itself, you can wind up in a bad place. Wherever you +manually commit data to your database, it's likely that some of your other code +is going to run *after* your commit. If that code goes on to do other important +things after that commit, and an error happens in the later code, you can +easily wind up with inconsistent data if you're not extremely careful. Some +data will have been written to the database that probably should not have. +Having a centralized commit point saves you from needing to think about this; +it's great for lazy people who also care about data integrity. Either the +request completes successfully, and all changes are committed, or it does not, +and all changes are aborted. + +Pyramid's transaction management system allows you to synchronize commits +between multiple databases. It also allows you to do things like conditionally +send email if a transaction commits, but otherwise keep quiet. + +Example: :ref:`bfg_sql_wiki_tutorial` (note the lack of commit statements +anywhere in application code). + +Configuration conflict detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a system is small, it's reasonably easy to keep it all in your head. But +when systems grow large, you may have hundreds or thousands of configuration +statements which add a view, add a route, and so forth. + +Pyramid's configuration system keeps track of your configuration statements. If +you accidentally add two that are identical, or Pyramid can't make sense out of +what it would mean to have both statements active at the same time, it will +complain loudly at startup time. It's not dumb though. It will automatically +resolve conflicting configuration statements on its own if you use the +configuration :meth:`~pyramid.config.Configurator.include` system. "More local" +statements are preferred over "less local" ones. This allows you to +intelligently factor large systems into smaller ones. + +Example: :ref:`conflict_detection`. + +Configuration extensibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike other systems, Pyramid provides a structured "include" mechanism (see +:meth:`~pyramid.config.Configurator.include`) that allows you to combine +applications from multiple Python packages. All the configuration statements +that can be performed in your "main" Pyramid application can also be performed +by included packages, including the addition of views, routes, subscribers, and +even authentication and authorization policies. You can even extend or override +an existing application by including another application's configuration in +your own, overriding or adding new views and routes to it. This has the +potential to allow you to create a big application out of many other smaller +ones. For example, if you want to reuse an existing application that already +has a bunch of routes, you can just use the ``include`` statement with a +``route_prefix``. The new application will live within your application at an +URL prefix. It's not a big deal, and requires little up-front engineering +effort. + +For example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + config = Configurator() + config.include('pyramid_jinja2') + config.include('pyramid_exclog') + config.include('some.other.guys.package', route_prefix='/someotherguy') + +.. seealso:: + + See also :ref:`including_configuration` and + :ref:`building_an_extensible_app`. + +Flexible authentication and authorization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid includes a flexible, pluggable authentication and authorization system. +No matter where your user data is stored, or what scheme you'd like to use to +permit your users to access your data, you can use a predefined Pyramid +plugpoint to plug in your custom authentication and authorization code. If you +want to change these schemes later, you can just change it in one place rather +than everywhere in your code. It also ships with prebuilt well-tested +authentication and authorization schemes out of the box. But what if you don't +want to use Pyramid's built-in system? You don't have to. You can just write +your own bespoke security code as you would in any other system. + +Example: :ref:`enabling_authorization_policy`. + +Traversal +~~~~~~~~~ + +:term:`Traversal` is a concept stolen from :term:`Zope`. It allows you to +create a tree of resources, each of which can be addressed by one or more URLs. +Each of those resources can have one or more *views* associated with it. If +your data isn't naturally treelike, or you're unwilling to create a treelike +representation of your data, you aren't going to find traversal very useful. +However, traversal is absolutely fantastic for sites that need to be +arbitrarily extensible. It's a lot easier to add a node to a tree than it is to +shoehorn a route into an ordered list of other routes, or to create another +entire instance of an application to service a department and glue code to +allow disparate apps to share data. It's a great fit for sites that naturally +lend themselves to changing departmental hierarchies, such as content +management systems and document management systems. Traversal also lends +itself well to systems that require very granular security ("Bob can edit +*this* document" as opposed to "Bob can edit documents"). + +Examples: :ref:`hello_traversal_chapter` and +:ref:`much_ado_about_traversal_chapter`. + +Tweens +~~~~~~ + +Pyramid has a sort of internal WSGI-middleware-ish pipeline that can be hooked +by arbitrary add-ons named "tweens". The debug toolbar is a "tween", and the +``pyramid_tm`` transaction manager is also. Tweens are more useful than WSGI +:term:`middleware` in some circumstances because they run in the context of +Pyramid itself, meaning you have access to templates and other renderers, a +"real" request object, and other niceties. + +Example: :ref:`registering_tweens`. + +View response adapters +~~~~~~~~~~~~~~~~~~~~~~ + +A lot is made of the aesthetics of what *kinds* of objects you're allowed to +return from view callables in various frameworks. In a previous section in +this document, we showed you that, if you use a :term:`renderer`, you can +usually return a dictionary from a view callable instead of a full-on +:term:`Response` object. But some frameworks allow you to return strings or +tuples from view callables. When frameworks allow for this, code looks +slightly prettier, because fewer imports need to be done, and there is less +code. For example, compare this: + +.. code-block:: python + :linenos: + + def aview(request): + return "Hello world!" + +To this: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def aview(request): + return Response("Hello world!") + +The former is "prettier", right? + +Out of the box, if you define the former view callable (the one that simply +returns a string) in Pyramid, when it is executed, Pyramid will raise an +exception. This is because "explicit is better than implicit", in most cases, +and by default Pyramid wants you to return a :term:`Response` object from a +view callable. This is because there's usually a heck of a lot more to a +response object than just its body. But if you're the kind of person who +values such aesthetics, we have an easy way to allow for this sort of thing: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + from pyramid.response import Response + + def string_response_adapter(s): + response = Response(s) + response.content_type = 'text/html' + return response + + if __name__ == '__main__': + config = Configurator() + config.add_response_adapter(string_response_adapter, basestring) + +Do that once in your Pyramid application at startup. Now you can return +strings from any of your view callables, e.g.: + +.. code-block:: python + :linenos: + + def helloview(request): + return "Hello world!" + + def goodbyeview(request): + return "Goodbye world!" + +Oh noes! What if you want to indicate a custom content type? And a custom +status code? No fear: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def tuple_response_adapter(val): + status_int, content_type, body = val + response = Response(body) + response.content_type = content_type + response.status_int = status_int + return response + + def string_response_adapter(body): + response = Response(body) + response.content_type = 'text/html' + response.status_int = 200 + return response + + if __name__ == '__main__': + config = Configurator() + config.add_response_adapter(string_response_adapter, basestring) + config.add_response_adapter(tuple_response_adapter, tuple) + +Once this is done, both of these view callables will work: + +.. code-block:: python + :linenos: + + def aview(request): + return "Hello world!" + + def anotherview(request): + return (403, 'text/plain', "Forbidden") + +Pyramid defaults to explicit behavior, because it's the most generally useful, +but provides hooks that allow you to adapt the framework to localized aesthetic +desires. + +.. seealso:: + + See also :ref:`using_iresponse`. + +"Global" response object +~~~~~~~~~~~~~~~~~~~~~~~~ + +"Constructing these response objects in my view callables is such a chore! And +I'm way too lazy to register a response adapter, as per the prior section," you +say. Fine. Be that way: + +.. code-block:: python + :linenos: + + def aview(request): + response = request.response + response.body = 'Hello world!' + response.content_type = 'text/plain' + return response + +.. seealso:: + + See also :ref:`request_response_attr`. + +Automating repetitive configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Does Pyramid's configurator allow you to do something, but you're a little +adventurous and just want it a little less verbose? Or you'd like to offer up +some handy configuration feature to other Pyramid users without requiring that +we change Pyramid? You can extend Pyramid's :term:`Configurator` with your own +directives. For example, let's say you find yourself calling +:meth:`pyramid.config.Configurator.add_view` repetitively. Usually you can +take the boring away by using existing shortcuts, but let's say that this is a +case where there is no such shortcut: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + config = Configurator() + config.add_route('xhr_route', '/xhr/{id}') + config.add_view('my.package.GET_view', route_name='xhr_route', + xhr=True, permission='view', request_method='GET') + config.add_view('my.package.POST_view', route_name='xhr_route', + xhr=True, permission='view', request_method='POST') + config.add_view('my.package.HEAD_view', route_name='xhr_route', + xhr=True, permission='view', request_method='HEAD') + +Pretty tedious right? You can add a directive to the Pyramid configurator to +automate some of the tedium away: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def add_protected_xhr_views(config, module): + module = config.maybe_dotted(module) + for method in ('GET', 'POST', 'HEAD'): + view = getattr(module, 'xhr_%s_view' % method, None) + if view is not None: + config.add_view(view, route_name='xhr_route', xhr=True, + permission='view', request_method=method) + + config = Configurator() + config.add_directive('add_protected_xhr_views', add_protected_xhr_views) + +Once that's done, you can call the directive you've just added as a method of +the Configurator object: + +.. code-block:: python + :linenos: + + config.add_route('xhr_route', '/xhr/{id}') + config.add_protected_xhr_views('my.package') + +Your previously repetitive configuration lines have now morphed into one line. + +You can share your configuration code with others this way, too, by packaging +it up and calling :meth:`~pyramid.config.Configurator.add_directive` from +within a function called when another user uses the +:meth:`~pyramid.config.Configurator.include` method against your code. + +.. seealso:: + + See also :ref:`add_directive`. + +Programmatic introspection +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're building a large system that other users may plug code into, it's +useful to be able to get an enumeration of what code they plugged in *at +application runtime*. For example, you might want to show them a set of tabs +at the top of the screen based on an enumeration of views they registered. + +This is possible using Pyramid's :term:`introspector`. + +Here's an example of using Pyramid's introspector from within a view callable: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='bar') + def show_current_route_pattern(request): + introspector = request.registry.introspector + route_name = request.matched_route.name + route_intr = introspector.get('routes', route_name) + return Response(str(route_intr['pattern'])) + +.. seealso:: + + See also :ref:`using_introspection`. + +Python 3 compatibility +~~~~~~~~~~~~~~~~~~~~~~ + +Pyramid and most of its add-ons are Python 3 compatible. If you develop a +Pyramid application today, you won't need to worry that five years from now +you'll be backwatered because there are language features you'd like to use but +your framework doesn't support newer Python versions. + +Testing +~~~~~~~ + +Every release of Pyramid has 100% statement coverage via unit and integration +tests, as measured by the ``coverage`` tool available on PyPI. It also has +greater than 95% decision/condition coverage as measured by the +``instrumental`` tool available on PyPI. It is automatically tested by Travis, +and Jenkins on Python 2.7, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3 +after each commit to its GitHub repository. Official Pyramid add-ons are held +to a similar testing standard. We still find bugs in Pyramid and its official +add-ons, but we've noticed we find a lot more of them while working on other +projects that don't have a good testing regime. + +Travis: https://travis-ci.org/Pylons/pyramid +Jenkins: http://jenkins.pylonsproject.org/job/pyramid/ + +Support +~~~~~~~ + +It's our goal that no Pyramid question go unanswered. Whether you ask a +question on IRC, on the Pylons-discuss mailing list, or on StackOverflow, +you're likely to get a reasonably prompt response. We don't tolerate "support +trolls" or other people who seem to get their rocks off by berating fellow +users in our various official support channels. We try to keep it well-lit and +new-user-friendly. + +Example: Visit irc\://freenode.net#pyramid (the ``#pyramid`` channel on +irc.freenode.net in an IRC client) or the pylons-discuss maillist at +http://groups.google.com/group/pylons-discuss/. + +Documentation +~~~~~~~~~~~~~ + +It's a constant struggle, but we try to maintain a balance between completeness +and new-user-friendliness in the official narrative Pyramid documentation +(concrete suggestions for improvement are always appreciated, by the way). We +also maintain a "cookbook" of recipes, which are usually demonstrations of +common integration scenarios too specific to add to the official narrative +docs. In any case, the Pyramid documentation is comprehensive. + +Example: The :ref:`Pyramid Community Cookbook <cookbook:pyramid-cookbook>`. + +.. index:: + single: Pylons Project What Is The Pylons Project? --------------------------- :app:`Pyramid` is a member of the collection of software published under the Pylons Project. Pylons software is written by a loose-knit community of -contributors. The `Pylons Project website <http://docs.pylonsproject.org>`_ +contributors. The `Pylons Project website <http://pylonsproject.org>`_ includes details about how :app:`Pyramid` relates to the Pylons Project. .. index:: @@ -96,72 +916,70 @@ includes details about how :app:`Pyramid` relates to the Pylons Project. :app:`Pyramid` and Other Web Frameworks ------------------------------------------ -Until the end of 2010, :app:`Pyramid` was known as :mod:`repoze.bfg`; it was -merged into the Pylons project as :app:`Pyramid` in November of that year. +The first release of Pyramid's predecessor (named :mod:`repoze.bfg`) was made +in July of 2008. At the end of 2010, we changed the name of :mod:`repoze.bfg` +to :app:`Pyramid`. It was merged into the Pylons project as :app:`Pyramid` in +November of that year. -:app:`Pyramid` was inspired by :term:`Zope`, :term:`Pylons` (version -1.0) and :term:`Django`. As a result, :app:`Pyramid` borrows several -concepts and features from each, combining them into a unique web -framework. +:app:`Pyramid` was inspired by :term:`Zope`, :term:`Pylons` (version 1.0), and +:term:`Django`. As a result, :app:`Pyramid` borrows several concepts and +features from each, combining them into a unique web framework. -Many features of :app:`Pyramid` trace their origins back to -:term:`Zope`. Like Zope applications, :app:`Pyramid` applications -can be configured via a set of declarative configuration files. Like -Zope applications, :app:`Pyramid` applications can be easily -extended: if you obey certain constraints, the application you produce -can be reused, modified, re-integrated, or extended by third-party -developers without forking the original application. The concepts of -:term:`traversal` and declarative security in :app:`Pyramid` were -pioneered first in Zope. +Many features of :app:`Pyramid` trace their origins back to :term:`Zope`. Like +Zope applications, :app:`Pyramid` applications can be easily extended. If you +obey certain constraints, the application you produce can be reused, modified, +re-integrated, or extended by third-party developers without forking the +original application. The concepts of :term:`traversal` and declarative +security in :app:`Pyramid` were pioneered first in Zope. The :app:`Pyramid` concept of :term:`URL dispatch` is inspired by the -:term:`Routes` system used by :term:`Pylons` version 1.0. Like Pylons -version 1.0, :app:`Pyramid` is mostly policy-free. It makes no -assertions about which database you should use, and its built-in -templating facilities are included only for convenience. In essence, -it only supplies a mechanism to map URLs to :term:`view` code, along -with a set of conventions for calling those views. You are free to -use third-party components that fit your needs in your applications. - -The concept of :term:`view` is used by :app:`Pyramid` mostly as it would be -by Django. :app:`Pyramid` has a documentation culture more like Django's -than like Zope's. - -Like :term:`Pylons` version 1.0, but unlike :term:`Zope`, a -:app:`Pyramid` application developer may use completely imperative -code to perform common framework configuration tasks such as adding a -view or a route. In Zope, :term:`ZCML` is typically required for -similar purposes. In :term:`Grok`, a Zope-based web framework, -:term:`decorator` objects and class-level declarations are used for -this purpose. :app:`Pyramid` supports :term:`ZCML` and -decorator-based configuration, but does not require either. See -:ref:`configuration_narr` for more information. - -Also unlike :term:`Zope` and unlike other "full-stack" frameworks such -as :term:`Django`, :app:`Pyramid` makes no assumptions about which -persistence mechanisms you should use to build an application. Zope -applications are typically reliant on :term:`ZODB`; :app:`Pyramid` -allows you to build :term:`ZODB` applications, but it has no reliance -on the ZODB software. Likewise, :term:`Django` tends to assume that -you want to store your application's data in a relational database. -:app:`Pyramid` makes no such assumption; it allows you to use a -relational database but doesn't encourage or discourage the decision. - -Other Python web frameworks advertise themselves as members of a class -of web frameworks named `model-view-controller -<http://en.wikipedia.org/wiki/Model–view–controller>`_ frameworks. -Insofar as this term has been claimed to represent a class of web -frameworks, :app:`Pyramid` also generally fits into this class. - -.. sidebar:: You Say :app:`Pyramid` is MVC, But Where's The Controller? - - The :app:`Pyramid` authors believe that the MVC pattern just doesn't - really fit the web very well. In a :app:`Pyramid` application, there is a - resource tree, which represents the site structure, and views, which tend - to present the data stored in the resource tree and a user-defined "domain - model". However, no facility provided *by the framework* actually - necessarily maps to the concept of a "controller" or "model". So if you - had to give it some acronym, I guess you'd say :app:`Pyramid` is actually - an "RV" framework rather than an "MVC" framework. "MVC", however, is - close enough as a general classification moniker for purposes of - comparison with other web frameworks. +:term:`Routes` system used by :term:`Pylons` version 1.0. Like Pylons version +1.0, :app:`Pyramid` is mostly policy-free. It makes no assertions about which +database you should use. Pyramid no longer has built-in templating facilities +as of version 1.5a2, but instead officially supports bindings for templating +languages, including Chameleon, Jinja2, and Mako. In essence, it only supplies +a mechanism to map URLs to :term:`view` code, along with a set of conventions +for calling those views. You are free to use third-party components that fit +your needs in your applications. + +The concept of :term:`view` is used by :app:`Pyramid` mostly as it would be by +Django. :app:`Pyramid` has a documentation culture more like Django's than +like Zope's. + +Like :term:`Pylons` version 1.0, but unlike :term:`Zope`, a :app:`Pyramid` +application developer may use completely imperative code to perform common +framework configuration tasks such as adding a view or a route. In Zope, +:term:`ZCML` is typically required for similar purposes. In :term:`Grok`, a +Zope-based web framework, :term:`decorator` objects and class-level +declarations are used for this purpose. Out of the box, Pyramid supports +imperative and decorator-based configuration. :term:`ZCML` may be used via an +add-on package named ``pyramid_zcml``. + +Also unlike :term:`Zope` and other "full-stack" frameworks such as +:term:`Django`, :app:`Pyramid` makes no assumptions about which persistence +mechanisms you should use to build an application. Zope applications are +typically reliant on :term:`ZODB`. :app:`Pyramid` allows you to build +:term:`ZODB` applications, but it has no reliance on the ZODB software. +Likewise, :term:`Django` tends to assume that you want to store your +application's data in a relational database. :app:`Pyramid` makes no such +assumption, allowing you to use a relational database, and neither encouraging +nor discouraging the decision. + +Other Python web frameworks advertise themselves as members of a class of web +frameworks named `model-view-controller +<http://en.wikipedia.org/wiki/Model–view–controller>`_ frameworks. Insofar as +this term has been claimed to represent a class of web frameworks, +:app:`Pyramid` also generally fits into this class. + +.. sidebar:: You Say :app:`Pyramid` is MVC, but Where's the Controller? + + The :app:`Pyramid` authors believe that the MVC pattern just doesn't really + fit the web very well. In a :app:`Pyramid` application, there is a resource + tree which represents the site structure, and views which tend to present + the data stored in the resource tree and a user-defined "domain model". + However, no facility provided *by the framework* actually necessarily maps + to the concept of a "controller" or "model". So if you had to give it some + acronym, I guess you'd say :app:`Pyramid` is actually an "RV" framework + rather than an "MVC" framework. "MVC", however, is close enough as a + general classification moniker for purposes of comparison with other web + frameworks. diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst new file mode 100644 index 000000000..98315ac9f --- /dev/null +++ b/docs/narr/introspector.rst @@ -0,0 +1,593 @@ +.. index:: + single: introspection + single: introspector + +.. _using_introspection: + +Pyramid Configuration Introspection +=================================== + +.. versionadded:: 1.3 + +When Pyramid starts up, each call to a :term:`configuration directive` causes +one or more :term:`introspectable` objects to be registered with an +:term:`introspector`. The introspector can be queried by application code to +obtain information about the configuration of the running application. This +feature is useful for debug toolbars, command-line scripts which show some +aspect of configuration, and for runtime reporting of startup-time +configuration settings. + +Using the Introspector +---------------------- + +Here's an example of using Pyramid's introspector from within a view callable: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='bar') + def show_current_route_pattern(request): + introspector = request.registry.introspector + route_name = request.matched_route.name + route_intr = introspector.get('routes', route_name) + return Response(str(route_intr['pattern'])) + +This view will return a response that contains the "pattern" argument provided +to the ``add_route`` method of the route which matched when the view was +called. It uses the :meth:`pyramid.interfaces.IIntrospector.get` method to +return an introspectable in the category ``routes`` with a +:term:`discriminator` equal to the matched route name. It then uses the +returned introspectable to obtain a "pattern" value. + +The introspectable returned by the query methods of the introspector has +methods and attributes described by +:class:`pyramid.interfaces.IIntrospectable`. In particular, the +:meth:`~pyramid.interfaces.IIntrospector.get`, +:meth:`~pyramid.interfaces.IIntrospector.get_category`, +:meth:`~pyramid.interfaces.IIntrospector.categories`, +:meth:`~pyramid.interfaces.IIntrospector.categorized`, and +:meth:`~pyramid.interfaces.IIntrospector.related` methods of an introspector +can be used to query for introspectables. + +Introspectable Objects +---------------------- + +Introspectable objects are returned from query methods of an introspector. Each +introspectable object implements the attributes and methods documented at +:class:`pyramid.interfaces.IIntrospectable`. + +The important attributes shared by all introspectables are the following: + +``title`` + + A human-readable text title describing the introspectable + +``category_name`` + + A text category name describing the introspection category to which this + introspectable belongs. It is often a plural if there are expected to be + more than one introspectable registered within the category. + +``discriminator`` + + A hashable object representing the unique value of this introspectable within + its category. + +``discriminator_hash`` + + The integer hash of the discriminator (useful in HTML links). + +``type_name`` + + The text name of a subtype within this introspectable's category. If there + is only one type name in this introspectable's category, this value will + often be a singular version of the category name but it can be an arbitrary + value. + +``action_info`` + + An object describing the directive call site which caused this introspectable + to be registered. It contains attributes described in + :class:`pyramid.interfaces.IActionInfo`. + +Besides having the attributes described above, an introspectable is a +dictionary-like object. An introspectable can be queried for data values via +its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods. +For example: + +.. code-block:: python + :linenos: + + route_intr = introspector.get('routes', 'edit_user') + pattern = route_intr['pattern'] + +Pyramid Introspection Categories +-------------------------------- + +The list of concrete introspection categories provided by built-in Pyramid +configuration directives follows. Add-on packages may supply other +introspectables in categories not described here. + +``subscribers`` + + Each introspectable in the ``subscribers`` category represents a call to + :meth:`pyramid.config.Configurator.add_subscriber` (or the decorator + equivalent). Each will have the following data. + + ``subscriber`` + + The subscriber callable object (the resolution of the ``subscriber`` + argument passed to ``add_subscriber``). + + ``interfaces`` + + A sequence of interfaces (or classes) that are subscribed to (the + resolution of the ``ifaces`` argument passed to ``add_subscriber``). + + ``derived_subscriber`` + + A wrapper around the subscriber used internally by the system so it can + call it with more than one argument if your original subscriber accepts + only one. + + ``predicates`` + + The predicate objects created as the result of passing predicate arguments + to ``add_subscriber``. + + ``derived_predicates`` + + Wrappers around the predicate objects created as the result of passing + predicate arguments to ``add_subscriber`` (to be used when predicates take + only one value but must be passed more than one). + +``response adapters`` + + Each introspectable in the ``response adapters`` category represents a call + to :meth:`pyramid.config.Configurator.add_response_adapter` (or a decorator + equivalent). Each will have the following data. + + ``adapter`` + + The adapter object (the resolved ``adapter`` argument to + ``add_response_adapter``). + + ``type`` + + The resolved ``type_or_iface`` argument passed to ``add_response_adapter``. + +``root factories`` + + Each introspectable in the ``root factories`` category represents a call to + :meth:`pyramid.config.Configurator.set_root_factory` (or the Configurator + constructor equivalent) *or* a ``factory`` argument passed to + :meth:`pyramid.config.Configurator.add_route`. Each will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_root_factory``). + + ``route_name`` + + The name of the route which will use this factory. If this is the + *default* root factory (if it's registered during a call to + ``set_root_factory``), this value will be ``None``. + +``session factory`` + + Only one introspectable will exist in the ``session factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_session_factory` + (or the Configurator constructor equivalent). It will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_session_factory``). + +``request factory`` + + Only one introspectable will exist in the ``request factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_request_factory` + (or the Configurator constructor equivalent). It will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_request_factory``). + +``locale negotiator`` + + Only one introspectable will exist in the ``locale negotiator`` category. It + represents a call to + :meth:`pyramid.config.Configurator.set_locale_negotiator` (or the + Configurator constructor equivalent). It will have the following data. + + ``negotiator`` + + The factory object (the resolved ``negotiator`` argument to + ``set_locale_negotiator``). + +``renderer factories`` + + Each introspectable in the ``renderer factories`` category represents a call + to :meth:`pyramid.config.Configurator.add_renderer` (or the Configurator + constructor equivalent). Each will have the following data. + + ``name`` + + The name of the renderer (the value of the ``name`` argument to + ``add_renderer``). + + ``factory`` + + The factory object (the resolved ``factory`` argument to ``add_renderer``). + +``routes`` + + Each introspectable in the ``routes`` category represents a call to + :meth:`pyramid.config.Configurator.add_route`. Each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_route``. + + ``pattern`` + + The ``pattern`` argument passed to ``add_route``. + + ``factory`` + + The (resolved) ``factory`` argument passed to ``add_route``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_route``. + + ``request_method`` + + The ``request_method`` argument passed to ``add_route``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_route`` or the value ``None`` if a + ``request_method`` argument was not supplied. + + ``path_info`` + + The ``path_info`` argument passed to ``add_route``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_route``. + + ``header`` + + The ``header`` argument passed to ``add_route``. + + ``accept`` + + The ``accept`` argument passed to ``add_route``. + + ``traverse`` + + The ``traverse`` argument passed to ``add_route``. + + ``custom_predicates`` + + The ``custom_predicates`` argument passed to ``add_route``. + + ``pregenerator`` + + The ``pregenerator`` argument passed to ``add_route``. + + ``static`` + + The ``static`` argument passed to ``add_route``. + + ``use_global_views`` + + The ``use_global_views`` argument passed to ``add_route``. + + ``object`` + + The :class:`pyramid.interfaces.IRoute` object that is used to perform + matching and generation for this route. + +``authentication policy`` + + There will be one and only one introspectable in the ``authentication + policy`` category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authentication_policy` method (or + its Configurator constructor equivalent). It will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authentication_policy``). + +``authorization policy`` + + There will be one and only one introspectable in the ``authorization policy`` + category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authorization_policy` method (or its + Configurator constructor equivalent). It will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authorization_policy``). + +``default permission`` + + There will be one and only one introspectable in the ``default permission`` + category. It represents a call to the + :meth:`pyramid.config.Configurator.set_default_permission` method (or its + Configurator constructor equivalent). It will have the following data. + + ``value`` + + The permission name passed to ``set_default_permission``. + +``views`` + + Each introspectable in the ``views`` category represents a call to + :meth:`pyramid.config.Configurator.add_view`. Each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_view``. + + ``context`` + + The (resolved) ``context`` argument passed to ``add_view``. + + ``containment`` + + The (resolved) ``containment`` argument passed to ``add_view``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_view``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_view`` or the value ``None`` if a + ``request_method`` argument was not supplied. + + ``route_name`` + + The ``route_name`` argument passed to ``add_view``. + + ``attr`` + + The ``attr`` argument passed to ``add_view``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_view``. + + ``accept`` + + The ``accept`` argument passed to ``add_view``. + + ``header`` + + The ``header`` argument passed to ``add_view``. + + ``path_info`` + + The ``path_info`` argument passed to ``add_view``. + + ``match_param`` + + The ``match_param`` argument passed to ``add_view``. + + ``csrf_token`` + + The ``csrf_token`` argument passed to ``add_view``. + + ``callable`` + + The (resolved) ``view`` argument passed to ``add_view``. Represents the + "raw" view callable. + + ``derived_callable`` + + The view callable derived from the ``view`` argument passed to + ``add_view``. Represents the view callable which Pyramid itself calls + (wrapped in security and other wrappers). + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view``. + + ``decorator`` + + The (resolved) ``decorator`` argument passed to ``add_view``. + +``permissions`` + + Each introspectable in the ``permissions`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit + ``permission`` argument *or* a call to + :meth:`pyramid.config.Configurator.set_default_permission`. Each will have + the following data. + + ``value`` + + The permission name passed to ``add_view`` or ``set_default_permission``. + +``templates`` + + Each introspectable in the ``templates`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has a ``renderer`` + argument which points to a template. Each will have the following data. + + ``name`` + + The renderer's name (a string). + + ``type`` + + The renderer's type (a string). + + ``renderer`` + + The :class:`pyramid.interfaces.IRendererInfo` object which represents this + template's renderer. + +``view mappers`` + + Each introspectable in the ``view mappers`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit ``mapper`` + argument *or* a call to + :meth:`pyramid.config.Configurator.set_view_mapper`. Each will have + the following data. + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view`` or + ``set_view_mapper``. + +``asset overrides`` + + Each introspectable in the ``asset overrides`` category represents a call to + :meth:`pyramid.config.Configurator.override_asset`. Each will have the + following data. + + ``to_override`` + + The ``to_override`` argument (an asset spec) passed to ``override_asset``. + + ``override_with`` + + The ``override_with`` argument (an asset spec) passed to + ``override_asset``. + +``translation directories`` + + Each introspectable in the ``translation directories`` category represents an + individual element in a ``specs`` argument passed to + :meth:`pyramid.config.Configurator.add_translation_dirs`. Each will have the + following data. + + ``directory`` + + The absolute path of the translation directory. + + ``spec`` + + The asset specification passed to ``add_translation_dirs``. + +``tweens`` + + Each introspectable in the ``tweens`` category represents a call to + :meth:`pyramid.config.Configurator.add_tween`. Each will have the following + data. + + ``name`` + + The dotted name to the tween factory as a string (passed as the + ``tween_factory`` argument to ``add_tween``). + + ``factory`` + + The (resolved) tween factory object. + + ``type`` + + ``implicit`` or ``explicit`` as a string. + + ``under`` + + The ``under`` argument passed to ``add_tween`` (a string). + + ``over`` + + The ``over`` argument passed to ``add_tween`` (a string). + +``static views`` + + Each introspectable in the ``static views`` category represents a call to + :meth:`pyramid.config.Configurator.add_static_view`. Each will have the + following data. + + ``name`` + + The ``name`` argument provided to ``add_static_view``. + + ``spec`` + + A normalized version of the ``spec`` argument provided to + ``add_static_view``. + +``traversers`` + + Each introspectable in the ``traversers`` category represents a call to + :meth:`pyramid.config.Configurator.add_traverser`. Each will have the + following data. + + ``iface`` + + The (resolved) interface or class object that represents the return value + of a root factory for which this traverser will be used. + + ``adapter`` + + The (resolved) traverser class. + +``resource url adapters`` + + Each introspectable in the ``resource url adapters`` category represents a + call to :meth:`pyramid.config.Configurator.add_resource_url_adapter`. Each + will have the following data. + + ``adapter`` + + The (resolved) resource URL adapter class. + + ``resource_iface`` + + The (resolved) interface or class object that represents the resource + interface for which this URL adapter is registered. + + ``request_iface`` + + The (resolved) interface or class object that represents the request + interface for which this URL adapter is registered. + +Introspection in the Toolbar +---------------------------- + +The Pyramid debug toolbar (part of the ``pyramid_debugtoolbar`` package) +provides a canned view of all registered introspectables and their +relationships. It is currently under the "Global" tab in the main navigation, +and it looks something like this: + +.. image:: tb_introspector.png + +Disabling Introspection +----------------------- + +You can disable Pyramid introspection by passing the flag +``introspection=False`` to the :term:`Configurator` constructor in your +application setup: + +.. code-block:: python + + from pyramid.config import Configurator + config = Configurator(..., introspection=False) + +When ``introspection`` is ``False``, all introspectables generated by +configuration directives are thrown away. diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst new file mode 100644 index 000000000..9c6e8a319 --- /dev/null +++ b/docs/narr/logging.rst @@ -0,0 +1,430 @@ +.. _logging_chapter: + +Logging +======= + +:app:`Pyramid` allows you to make use of the Python standard library +:mod:`logging` module. This chapter describes how to configure logging and how +to send log messages to loggers that you've configured. + +.. warning:: + + This chapter assumes you've used a :term:`scaffold` to create a project + which contains ``development.ini`` and ``production.ini`` files which help + configure logging. All of the scaffolds which ship with :app:`Pyramid` do + this. If you're not using a scaffold, or if you've used a third-party + scaffold which does not create these files, the configuration information in + this chapter may not be applicable. + +.. index: + pair: settings; logging + pair: .ini; logging + pair: logging; configuration + +.. _logging_config: + +Logging Configuration +--------------------- + +A :app:`Pyramid` project created from a :term:`scaffold` is configured to allow +you to send messages to :mod:`Python standard library logging package +<logging>` loggers from within your application. In particular, the +:term:`PasteDeploy` ``development.ini`` and ``production.ini`` files created +when you use a scaffold include a basic configuration for the Python +:mod:`logging` package. + +PasteDeploy ``.ini`` files use the Python standard library :mod:`ConfigParser +format <ConfigParser>`. This is the same format used as the Python +:ref:`logging module's Configuration file format <logging-config-fileformat>`. +The application-related and logging-related sections in the configuration file +can coexist peacefully, and the logging-related sections in the file are used +from when you run ``pserve``. + +The ``pserve`` command calls the :func:`pyramid.paster.setup_logging` function, +a thin wrapper around the :func:`logging.config.fileConfig` using the specified +``.ini`` file, if it contains a ``[loggers]`` section (all of the +scaffold-generated ``.ini`` files do). ``setup_logging`` reads the logging +configuration from the ini file upon which ``pserve`` was invoked. + +Default logging configuration is provided in both the default +``development.ini`` and the ``production.ini`` file. The logging configuration +in the ``development.ini`` file is as follows: + +.. code-block:: ini + :linenos: + + # Begin logging configuration + + [loggers] + keys = root, {{package_logger}} + + [handlers] + keys = console + + [formatters] + keys = generic + + [logger_root] + level = INFO + handlers = console + + [logger_{{package_logger}}] + level = DEBUG + handlers = + qualname = {{package}} + + [handler_console] + class = StreamHandler + args = (sys.stderr,) + level = NOTSET + formatter = generic + + [formatter_generic] + format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + + # End logging configuration + +The ``production.ini`` file uses the ``WARN`` level in its logger +configuration, but it is otherwise identical. + +The name ``{{package_logger}}`` above will be replaced with the name of your +project's :term:`package`, which is derived from the name you provide to your +project. For instance, if you do: + +.. code-block:: text + :linenos: + + pcreate -s starter MyApp + +The logging configuration will literally be: + +.. code-block:: ini + :linenos: + + # Begin logging configuration + + [loggers] + keys = root, myapp + + [handlers] + keys = console + + [formatters] + keys = generic + + [logger_root] + level = INFO + handlers = console + + [logger_myapp] + level = DEBUG + handlers = + qualname = myapp + + [handler_console] + class = StreamHandler + args = (sys.stderr,) + level = NOTSET + formatter = generic + + [formatter_generic] + format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + + # End logging configuration + +In this logging configuration: + +- a logger named ``root`` is created that logs messages at a level above or + equal to the ``INFO`` level to stderr, with the following format: + + .. code-block:: text + + 2007-08-17 15:04:08,704 INFO [packagename] Loading resource, id: 86 + +- a logger named ``myapp`` is configured that logs messages sent at a level + above or equal to ``DEBUG`` to stderr in the same format as the root logger. + +The ``root`` logger will be used by all applications in the Pyramid process +that ask for a logger (via ``logging.getLogger``) that has a name which begins +with anything except your project's package name (e.g., ``myapp``). The logger +with the same name as your package name is reserved for your own usage in your +:app:`Pyramid` application. Its existence means that you can log to a known +logging location from any :app:`Pyramid` application generated via a scaffold. + +:app:`Pyramid` and many other libraries (such as Beaker, SQLAlchemy, Paste) log +a number of messages to the root logger for debugging purposes. Switching the +root logger level to ``DEBUG`` reveals them: + +.. code-block:: ini + + [logger_root] + #level = INFO + level = DEBUG + handlers = console + +Some scaffolds configure additional loggers for additional subsystems they use +(such as SQLALchemy). Take a look at the ``production.ini`` and +``development.ini`` files rendered when you create a project from a scaffold. + +Sending Logging Messages +------------------------ + +Python's special ``__name__`` variable refers to the current module's fully +qualified name. From any module in a package named ``myapp``, the ``__name__`` +builtin variable will always be something like ``myapp``, or +``myapp.subpackage`` or ``myapp.package.subpackage`` if your project is named +``myapp``. Sending a message to this logger will send it to the ``myapp`` +logger. + +To log messages to the package-specific logger configured in your ``.ini`` +file, simply create a logger object using the ``__name__`` builtin and call +methods on it. + +.. code-block:: python + :linenos: + + import logging + log = logging.getLogger(__name__) + + def myview(request): + content_type = 'text/plain' + content = 'Hello World!' + log.debug('Returning: %s (content-type: %s)', content, content_type) + request.response.content_type = content_type + return request.response + +This will result in the following printed to the console, on ``stderr``: + +.. code-block:: text + + 16:20:20,440 DEBUG [myapp.views] Returning: Hello World! + (content-type: text/plain) + +Filtering log messages +---------------------- + +Often there's too much log output to sift through, such as when switching the +root logger's level to ``DEBUG``. + +For example, you're diagnosing database connection issues in your application +and only want to see SQLAlchemy's ``DEBUG`` messages in relation to database +connection pooling. You can leave the root logger's level at the less verbose +``INFO`` level and set that particular SQLAlchemy logger to ``DEBUG`` on its +own, apart from the root logger: + +.. code-block:: ini + + [logger_sqlalchemy.pool] + level = DEBUG + handlers = + qualname = sqlalchemy.pool + +then add it to the list of loggers: + +.. code-block:: ini + + [loggers] + keys = root, myapp, sqlalchemy.pool + +No handlers need to be configured for this logger as by default non-root +loggers will propagate their log records up to their parent logger's handlers. +The root logger is the top level parent of all loggers. + +This technique is used in the default ``development.ini``. The root logger's +level is set to ``INFO``, whereas the application's log level is set to +``DEBUG``: + +.. code-block:: ini + + # Begin logging configuration + + [loggers] + keys = root, myapp + + [logger_myapp] + level = DEBUG + handlers = + qualname = myapp + +All of the child loggers of the ``myapp`` logger will inherit the ``DEBUG`` +level unless they're explicitly set differently. Meaning the ``myapp.views``, +``myapp.models``, and all your app's modules' loggers by default have an +effective level of ``DEBUG`` too. + +For more advanced filtering, the logging module provides a +:class:`logging.Filter` object; however it cannot be used directly from the +configuration file. + +Advanced Configuration +---------------------- + +To capture log output to a separate file, use :class:`logging.FileHandler` (or +:class:`logging.handlers.RotatingFileHandler`): + +.. code-block:: ini + + [handler_filelog] + class = FileHandler + args = ('%(here)s/myapp.log','a') + level = INFO + formatter = generic + +Before it's recognized, it needs to be added to the list of handlers: + +.. code-block:: ini + + [handlers] + keys = console, myapp, filelog + +and finally utilized by a logger. + +.. code-block:: ini + + [logger_root] + level = INFO + handlers = console, filelog + +These final three lines of configuration direct all of the root logger's output +to the ``myapp.log`` as well as the console. + +Logging Exceptions +------------------ + +To log or email exceptions generated by your :app:`Pyramid` application, use +the :term:`pyramid_exclog` package. Details about its configuration are in its +`documentation <http://docs.pylonsproject.org/projects/pyramid_exclog/dev/>`_. + +.. index:: + single: TransLogger + single: middleware; TransLogger + pair: configuration; middleware + single: settings; middleware + pair: .ini; middleware + +.. _request_logging_with_pastes_translogger: + +Request Logging with Paste's TransLogger +---------------------------------------- + +The :term:`WSGI` design is modular. Waitress logs error conditions, debugging +output, etc., but not web traffic. For web traffic logging, Paste provides the +`TransLogger <http://pythonpaste.org/modules/translogger.html>`_ +:term:`middleware`. TransLogger produces logs in the `Apache Combined Log +Format <http://httpd.apache.org/docs/2.2/logs.html#combined>`_. But +TransLogger does not write to files; the Python logging system must be +configured to do this. The Python :class:`logging.FileHandler` logging handler +can be used alongside TransLogger to create an ``access.log`` file similar to +Apache's. + +Like any standard :term:`middleware` with a Paste entry point, TransLogger can +be configured to wrap your application using ``.ini`` file syntax. First +rename your Pyramid ``.ini`` file's ``[app:main]`` section to +``[app:mypyramidapp]``, then add a ``[filter:translogger]`` section, then use a +``[pipeline:main]`` section file to form a WSGI pipeline with both the +translogger and your application in it. For instance, change from this: + +.. code-block:: ini + + [app:main] + use = egg:MyProject + +To this: + +.. code-block:: ini + + [app:mypyramidapp] + use = egg:MyProject + + [filter:translogger] + use = egg:Paste#translogger + setup_console_handler = False + + [pipeline:main] + pipeline = translogger + mypyramidapp + +Using PasteDeploy this way to form and serve a pipeline is equivalent to +wrapping your app in a TransLogger instance via the bottom of the ``main`` +function of your project's ``__init__`` file: + +.. code-block:: python + + ... + app = config.make_wsgi_app() + from paste.translogger import TransLogger + app = TransLogger(app, setup_console_handler=False) + return app + +.. note:: + TransLogger will automatically setup a logging handler to the console when + called with no arguments, so it "just works" in environments that don't + configure logging. Since our logging handlers are configured, we disable + the automation via ``setup_console_handler = False``. + +With the filter in place, TransLogger's logger (named the ``wsgi`` logger) will +propagate its log messages to the parent logger (the root logger), sending its +output to the console when we request a page: + +.. code-block:: text + + 00:50:53,694 INFO [myapp.views] Returning: Hello World! + (content-type: text/plain) + 00:50:53,695 INFO [wsgi] 192.168.1.111 - - [11/Aug/2011:20:09:33 -0700] "GET /hello + HTTP/1.1" 404 - "-" + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725 + Firefox/2.0.0.6" + +To direct TransLogger to an ``access.log`` FileHandler, we need the following +to add a FileHandler (named ``accesslog``) to the list of handlers, and ensure +that the ``wsgi`` logger is configured and uses this handler accordingly: + +.. code-block:: ini + + # Begin logging configuration + + [loggers] + keys = root, myapp, wsgi + + [handlers] + keys = console, accesslog + + [logger_wsgi] + level = INFO + handlers = accesslog + qualname = wsgi + propagate = 0 + + [handler_accesslog] + class = FileHandler + args = ('%(here)s/access.log','a') + level = INFO + formatter = generic + +As mentioned above, non-root loggers by default propagate their log records to +the root logger's handlers (currently the console handler). Setting +``propagate`` to ``0`` (``False``) here disables this; so the ``wsgi`` logger +directs its records only to the ``accesslog`` handler. + +Finally, there's no need to use the ``generic`` formatter with TransLogger as +TransLogger itself provides all the information we need. We'll use a formatter +that passes through the log messages as is. Add a new formatter called +``accesslog`` by including the following in your configuration file: + +.. code-block:: ini + + [formatters] + keys = generic, accesslog + + [formatter_accesslog] + format = %(message)s + +Finally alter the existing configuration to wire this new ``accesslog`` +formatter into the FileHandler: + +.. code-block:: ini + + [handler_accesslog] + class = FileHandler + args = ('%(here)s/access.log','a') + level = INFO + formatter = accesslog diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index a4709ef18..3e00a295a 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -4,72 +4,78 @@ Much Ado About Traversal ======================== -.. note:: This chapter was adapted, with permission, from a blog post by `Rob - Miller <http://blog.nonsequitarian.org/>`_, originally published at - http://blog.nonsequitarian.org/2010/much-ado-about-traversal/ . +(Or, why you should care about it.) -Traversal is an alternative to :term:`URL dispatch` which allows -:app:`Pyramid` applications to map URLs to code. +.. note:: + + This chapter was adapted, with permission, from a blog post by `Rob Miller + <http://blog.nonsequitarian.org/>`_, originally published at + http://blog.nonsequitarian.org/2010/much-ado-about-traversal/. + +Traversal is an alternative to :term:`URL dispatch` which allows :app:`Pyramid` +applications to map URLs to code. .. note:: - Ex-Zope users whom are already familiar with traversal and view lookup + Ex-Zope users who are already familiar with traversal and view lookup conceptually may want to skip directly to the :ref:`traversal_chapter` - chapter, which discusses technical details. This chapter is mostly aimed - at people who have previous :term:`Pylons` experience or experience in - another framework which does not provide traversal, and need an - introduction to the "why" of traversal. + chapter, which discusses technical details. This chapter is mostly aimed at + people who have previous :term:`Pylons` experience or experience in another + framework which does not provide traversal, and need an introduction to the + "why" of traversal. Some folks who have been using Pylons and its Routes-based URL matching for a long time are being exposed for the first time, via :app:`Pyramid`, to new ideas such as ":term:`traversal`" and ":term:`view lookup`" as a way to route incoming HTTP requests to callable code. Some of the same folks believe that -traversal is hard to understand. Others question its usefulness; URL -matching has worked for them so far, why should they even consider dealing -with another approach, one which doesn't fit their brain and which doesn't -provide any immediately obvious value? +traversal is hard to understand. Others question its usefulness; URL matching +has worked for them so far, so why should they even consider dealing with +another approach, one which doesn't fit their brain and which doesn't provide +any immediately obvious value? You can be assured that if you don't want to understand traversal, you don't have to. You can happily build :app:`Pyramid` applications with only -:term:`URL dispatch`. However, there are some straightforward, real-world -use cases that are much more easily served by a traversal-based approach than -by a pattern-matching mechanism. Even if you haven't yet hit one of these -use cases yourself, understanding these new ideas is worth the effort for any -web developer so you know when you might want to use them. :term:`Traversal` -is actually a straightforward metaphor easily comprehended by anyone who's -ever used a run-of-the-mill file system with folders and files. +:term:`URL dispatch`. However, there are some straightforward, real-world use +cases that are much more easily served by a traversal-based approach than by a +pattern-matching mechanism. Even if you haven't yet hit one of these use cases +yourself, understanding these new ideas is worth the effort for any web +developer so you know when you might want to use them. :term:`Traversal` is +actually a straightforward metaphor easily comprehended by anyone who's ever +used a run-of-the-mill file system with folders and files. + +.. index:: + single: URL dispatch URL Dispatch ------------ -Let's step back and consider the problem we're trying to solve. An -HTTP request for a particular path has been routed to our web -application. The requested path will possibly invoke a specific -:term:`view callable` function defined somewhere in our app. We're -trying to determine *which* callable function, if any, should be -invoked for a given requested URL. +Let's step back and consider the problem we're trying to solve. An HTTP +request for a particular path has been routed to our web application. The +requested path will possibly invoke a specific :term:`view callable` function +defined somewhere in our app. We're trying to determine *which* callable +function, if any, should be invoked for a given requested URL. Many systems, including Pyramid, offer a simple solution. They offer the -concept of "URL matching". URL matching approaches this problem by parsing -the URL path and comparing the results to a set of registered "patterns", -defined by a set of regular expressions, or some other URL path templating -syntax. Each pattern is mapped to a callable function somewhere; if the -request path matches a specific pattern, the associated function is called. -If the request path matches more than one pattern, some conflict resolution -scheme is used, usually a simple order precedence so that the first match -will take priority over any subsequent matches. If a request path doesn't -match any of the defined patterns, a "404 Not Found" response is returned. - -In Pyramid, we offer an implementation of URL matching which we call -:term:`URL dispatch`. Using :app:`Pyramid` syntax, we might have a match -pattern such as ``/{userid}/photos/{photoid}``, mapped to a ``photo_view()`` -function defined somewhere in our code. Then a request for a path such as +concept of "URL matching". URL matching approaches this problem by parsing the +URL path and comparing the results to a set of registered "patterns", defined +by a set of regular expressions or some other URL path templating syntax. Each +pattern is mapped to a callable function somewhere; if the request path matches +a specific pattern, the associated function is called. If the request path +matches more than one pattern, some conflict resolution scheme is used, usually +a simple order precedence so that the first match will take priority over any +subsequent matches. If a request path doesn't match any of the defined +patterns, a "404 Not Found" response is returned. + +In Pyramid, we offer an implementation of URL matching which we call :term:`URL +dispatch`. Using :app:`Pyramid` syntax, we might have a match pattern such as +``/{userid}/photos/{photoid}``, mapped to a ``photo_view()`` function defined +somewhere in our code. Then a request for a path such as ``/joeschmoe/photos/photo1`` would be a match, and the ``photo_view()`` function would be invoked to handle the request. Similarly, -``/{userid}/blog/{year}/{month}/{postid}`` might map to a -``blog_post_view()`` function, so ``/joeschmoe/blog/2010/12/urlmatching`` -would trigger the function, which presumably would know how to find and -render the ``urlmatching`` blog post. +``/{userid}/blog/{year}/{month}/{postid}`` might map to a ``blog_post_view()`` +function, so ``/joeschmoe/blog/2010/12/urlmatching`` would trigger the +function, which presumably would know how to find and render the +``urlmatching`` blog post. Historical Refresher -------------------- @@ -81,65 +87,67 @@ time when we didn't have fancy web frameworks like :term:`Pylons` and :app:`Pyramid`. Instead, we had general purpose HTTP servers that primarily served files off of a file system. The "root" of a given site mapped to a particular folder somewhere on the file system. Each segment of the request -URL path represented a subdirectory. The final path segment would be either -a directory or a file, and once the server found the right file it would -package it up in an HTTP response and send it back to the client. So serving -up a request for ``/joeschmoe/photos/photo1`` literally meant that there was -a ``joeschmoe`` folder somewhere, which contained a ``photos`` folder, which -in turn contained a ``photo1`` file. If at any point along the way we find -that there is not a folder or file matching the requested path, we return a -404 response. +URL path represented a subdirectory. The final path segment would be either a +directory or a file, and once the server found the right file it would package +it up in an HTTP response and send it back to the client. So serving up a +request for ``/joeschmoe/photos/photo1`` literally meant that there was a +``joeschmoe`` folder somewhere, which contained a ``photos`` folder, which in +turn contained a ``photo1`` file. If at any point along the way we find that +there is not a folder or file matching the requested path, we return a 404 +response. As the web grew more dynamic, however, a little bit of extra complexity was -added. Technologies such as CGI and HTTP server modules were developed. -Files were still looked up on the file system, but if the file ended with -(for example) ``.cgi`` or ``.php``, or if it lived in a special folder, -instead of simply sending the file to the client the server would read the -file, execute it using an interpreter of some sort, and then send the output -from this process to the client as the final result. The server -configuration specified which files would trigger some dynamic code, with the -default case being to just serve the static file. - -Traversal (aka Resource Location) ---------------------------------- +added. Technologies such as CGI and HTTP server modules were developed. Files +were still looked up on the file system, but if the file ended with (for +example) ``.cgi`` or ``.php``, or if it lived in a special folder, instead of +simply sending the file to the client the server would read the file, execute +it using an interpreter of some sort, and then send the output from this +process to the client as the final result. The server configuration specified +which files would trigger some dynamic code, with the default case being to +just serve the static file. .. index:: - single: traversal overview + single: traversal + +Traversal (a.k.a., Resource Location) +------------------------------------- Believe it or not, if you understand how serving files from a file system works, you understand traversal. And if you understand that a server might do -something different based on what type of file a given request specifies, -then you understand view lookup. +something different based on what type of file a given request specifies, then +you understand view lookup. The major difference between file system lookup and traversal is that a file -system lookup steps through nested directories and files in a file system -tree, while traversal steps through nested dictionary-type objects in a -:term:`resource tree`. Let's take a detailed look at one of our example -paths, so we can see what I mean: - -The path ``/joeschmoe/photos/photo1``, has four segments: ``/``, -``joeschmoe``, ``photos`` and ``photo1``. With file system lookup we might -have a root folder (``/``) containing a nested folder (``joeschmoe``), which -contains another nested folder (``photos``), which finally contains a JPG -file (``photo1``). With traversal, we instead have a dictionary-like root -object. Asking for the ``joeschmoe`` key gives us another dictionary-like -object. Asking this in turn for the ``photos`` key gives us yet another -mapping object, which finally (hopefully) contains the resource that we're -looking for within its values, referenced by the ``photo1`` key. - -In pure Python terms, then, the traversal or "resource location" -portion of satisfying the ``/joeschmoe/photos/photo1`` request -will look something like this pseudocode:: +system lookup steps through nested directories and files in a file system tree, +while traversal steps through nested dictionary-type objects in a +:term:`resource tree`. Let's take a detailed look at one of our example paths, +so we can see what I mean. + +The path ``/joeschmoe/photos/photo1``, has four segments: ``/``, ``joeschmoe``, +``photos`` and ``photo1``. With file system lookup we might have a root folder +(``/``) containing a nested folder (``joeschmoe``), which contains another +nested folder (``photos``), which finally contains a JPG file (``photo1``). +With traversal, we instead have a dictionary-like root object. Asking for the +``joeschmoe`` key gives us another dictionary-like object. Asking in turn for +the ``photos`` key gives us yet another mapping object, which finally +(hopefully) contains the resource that we're looking for within its values, +referenced by the ``photo1`` key. + +In pure Python terms, then, the traversal or "resource location" portion of +satisfying the ``/joeschmoe/photos/photo1`` request will look something like +this pseudocode:: get_root()['joeschmoe']['photos']['photo1'] -``get_root()`` is some function that returns a root traversal -:term:`resource`. If all of the specified keys exist, then the returned -object will be the resource that is being requested, analogous to the JPG -file that was retrieved in the file system example. If a :exc:`KeyError` is -generated anywhere along the way, :app:`Pyramid` will return 404. (This -isn't precisely true, as you'll see when we learn about view lookup below, -but the basic idea holds.) +``get_root()`` is some function that returns a root traversal :term:`resource`. +If all of the specified keys exist, then the returned object will be the +resource that is being requested, analogous to the JPG file that was retrieved +in the file system example. If a :exc:`KeyError` is generated anywhere along +the way, :app:`Pyramid` will return 404. (This isn't precisely true, as you'll +see when we learn about view lookup below, but the basic idea holds.) + +.. index:: + single: resource What Is a "Resource"? --------------------- @@ -149,51 +157,47 @@ nested dictionary things? Where do these objects, these 'resources', live? What *are* they?" Since :app:`Pyramid` is not a highly opinionated framework, it makes no -restriction on how a :term:`resource` is implemented; a developer can -implement them as he wishes. One common pattern used is to persist all of -the resources, including the root, in a database as a graph. The root object -is a dictionary-like object. Dictionary-like objects in Python supply a +restriction on how a :term:`resource` is implemented; a developer can implement +them as they wish. One common pattern used is to persist all of the resources, +including the root, in a database as a graph. The root object is a +dictionary-like object. Dictionary-like objects in Python supply a ``__getitem__`` method which is called when key lookup is done. Under the hood, when ``adict`` is a dictionary-like object, Python translates ``adict['a']`` to ``adict.__getitem__('a')``. Try doing this in a Python interpreter prompt if you don't believe us: -.. code-block:: text - :linenos: - - Python 2.4.6 (#2, Apr 29 2010, 00:31:48) - [GCC 4.4.3] on linux2 - Type "help", "copyright", "credits" or "license" for more information. - >>> adict = {} - >>> adict['a'] = 1 - >>> adict['a'] - 1 - >>> adict.__getitem__('a') - 1 - +>>> adict = {} +>>> adict['a'] = 1 +>>> adict['a'] +1 +>>> adict.__getitem__('a') +1 The dictionary-like root object stores the ids of all of its subresources as keys, and provides a ``__getitem__`` implementation that fetches them. So ``get_root()`` fetches the unique root object, while ``get_root()['joeschmoe']`` returns a different object, also stored in the database, which in turn has its own subresources and ``__getitem__`` -implementation, etc. These resources might be persisted in a relational +implementation, and so on. These resources might be persisted in a relational database, one of the many "NoSQL" solutions that are becoming popular these -days, or anywhere else, it doesn't matter. As long as the returned objects -provide the dictionary-like API (i.e. as long as they have an appropriately -implemented ``__getitem__`` method) then traversal will work. - -In fact, you don't need a "database" at all. You could use plain -dictionaries, with your site's URL structure hard-coded directly in -the Python source. Or you could trivially implement a set of objects -with ``__getitem__`` methods that search for files in specific -directories, and thus precisely recreate the traditional mechanism of -having the URL path mapped directly to a folder structure on the file -system. Traversal is in fact a superset of file system lookup. +days, or anywhere else; it doesn't matter. As long as the returned objects +provide the dictionary-like API (i.e., as long as they have an appropriately +implemented ``__getitem__`` method), then traversal will work. + +In fact, you don't need a "database" at all. You could use plain dictionaries, +with your site's URL structure hard-coded directly in the Python source. Or +you could trivially implement a set of objects with ``__getitem__`` methods +that search for files in specific directories, and thus precisely recreate the +traditional mechanism of having the URL path mapped directly to a folder +structure on the file system. Traversal is in fact a superset of file system +lookup. .. note:: See the chapter entitled :ref:`resources_chapter` for a more technical overview of resources. +.. index:: + single: view lookup + View Lookup ----------- @@ -201,34 +205,33 @@ At this point we're nearly there. We've covered traversal, which is the process by which a specific resource is retrieved according to a specific URL path. But what is "view lookup"? -The need for view lookup is simple: there is more than one possible action -that you might want to take after finding a :term:`resource`. With our photo +The need for view lookup is simple: there is more than one possible action that +you might want to take after finding a :term:`resource`. With our photo example, for instance, you might want to view the photo in a page, but you might also want to provide a way for the user to edit the photo and any associated metadata. We'll call the former the ``view`` view, and the latter will be the ``edit`` view. (Original, I know.) :app:`Pyramid` has a centralized view :term:`application registry` where named views can be -associated with specific resource types. So in our example, we'll assume -that we've registered ``view`` and ``edit`` views for photo objects, and that -we've specified the ``view`` view as the default, so that +associated with specific resource types. So in our example, we'll assume that +we've registered ``view`` and ``edit`` views for photo objects, and that we've +specified the ``view`` view as the default, so that ``/joeschmoe/photos/photo1/view`` and ``/joeschmoe/photos/photo1`` are equivalent. The edit view would sensibly be provided by a request for ``/joeschmoe/photos/photo1/edit``. Hopefully it's clear that the first portion of the edit view's URL path is -going to resolve to the same resource as the non-edit version, specifically -the resource returned by ``get_root()['joeschmoe']['photos']['photo1']``. -But traveral ends there; the ``photo1`` resource doesn't have an ``edit`` -key. In fact, it might not even be a dictionary-like object, in which case +going to resolve to the same resource as the non-edit version, specifically the +resource returned by ``get_root()['joeschmoe']['photos']['photo1']``. But +traversal ends there; the ``photo1`` resource doesn't have an ``edit`` key. In +fact, it might not even be a dictionary-like object, in which case ``photo1['edit']`` would be meaningless. When the :app:`Pyramid` resource location has been resolved to a *leaf* resource, but the entire request path has not yet been expended, the *very next* path segment is treated as a -:term:`view name`. The registry is then checked to see if a view of the -given name has been specified for a resource of the given type. If so, the -view callable is invoked, with the resource passed in as the related -``context`` object (also available as ``request.context``). If a view -callable could not be found, :app:`Pyramid` will return a "404 Not Found" -response. +:term:`view name`. The registry is then checked to see if a view of the given +name has been specified for a resource of the given type. If so, the view +callable is invoked, with the resource passed in as the related ``context`` +object (also available as ``request.context``). If a view callable could not +be found, :app:`Pyramid` will return a "404 Not Found" response. You might conceptualize a request for ``/joeschmoe/photos/photo1/edit`` as ultimately converted into the following piece of Pythonic pseudocode:: @@ -239,8 +242,8 @@ ultimately converted into the following piece of Pythonic pseudocode:: view_callable(request) The ``get_root`` and ``get_view`` functions don't really exist. Internally, -:app:`Pyramid` does something more complicated. But the example above -is a reasonable approximation of the view lookup algorithm in pseudocode. +:app:`Pyramid` does something more complicated. But the example above is a +reasonable approximation of the view lookup algorithm in pseudocode. Use Cases --------- @@ -254,56 +257,57 @@ like this:: /{userid}/{typename}/{objectid}[/{view_name}] -In all of the examples thus far, we've hard coded the typename value, -assuming that we'd know at development time what names were going to be used -("photos", "blog", etc.). But what if we don't know what these names will -be? Or, worse yet, what if we don't know *anything* about the structure of -the URLs inside a user's folder? We could be writing a CMS where we want the -end user to be able to arbitrarily add content and other folders inside his -folder. He might decide to nest folders dozens of layers deep. How will you -construct matching patterns that could account for every possible combination -of paths that might develop? - -It might be possible, but it certainly won't be easy. The matching -patterns are going to become complex quickly as you try to handle all -of the edge cases. - -With traversal, however, it's straightforward. Twenty layers of nesting -would be no problem. :app:`Pyramid` will happily call ``__getitem__`` as -many times as it needs to, until it runs out of path segments or until a -resource raises a :exc:`KeyError`. Each resource only needs to know how to -fetch its immediate children, the traversal algorithm takes care of the rest. -Also, since the structure of the resource tree can live in the database and -not in the code, it's simple to let users modify the tree at runtime to set -up their own personalized "directory" structures. - -Another use case in which traversal shines is when there is a need to support -a context-dependent security policy. One example might be a document -management infrastructure for a large corporation, where members of different -departments have varying access levels to the various other departments' -files. Reasonably, even specific files might need to be made available to -specific individuals. Traversal does well here if your resources actually -represent the data objects related to your documents, because the idea of a -resource authorization is baked right into the code resolution and calling -process. Resource objects can store ACLs, which can be inherited and/or -overridden by the subresources. - -If each resource can thus generate a context-based ACL, then whenever view -code is attempting to perform a sensitive action, it can check against that -ACL to see whether the current user should be allowed to perform the action. -In this way you achieve so called "instance based" or "row level" security -which is considerably harder to model using a traditional tabular approach. +In all of the examples thus far, we've hard coded the typename value, assuming +that we'd know at development time what names were going to be used ("photos", +"blog", etc.). But what if we don't know what these names will be? Or, worse +yet, what if we don't know *anything* about the structure of the URLs inside a +user's folder? We could be writing a CMS where we want the end user to be able +to arbitrarily add content and other folders inside his folder. He might +decide to nest folders dozens of layers deep. How will you construct matching +patterns that could account for every possible combination of paths that might +develop? + +It might be possible, but it certainly won't be easy. The matching patterns +are going to become complex quickly as you try to handle all of the edge cases. + +With traversal, however, it's straightforward. Twenty layers of nesting would +be no problem. :app:`Pyramid` will happily call ``__getitem__`` as many times +as it needs to, until it runs out of path segments or until a resource raises a +:exc:`KeyError`. Each resource only needs to know how to fetch its immediate +children, and the traversal algorithm takes care of the rest. Also, since the +structure of the resource tree can live in the database and not in the code, +it's simple to let users modify the tree at runtime to set up their own +personalized "directory" structures. + +Another use case in which traversal shines is when there is a need to support a +context-dependent security policy. One example might be a document management +infrastructure for a large corporation, where members of different departments +have varying access levels to the various other departments' files. +Reasonably, even specific files might need to be made available to specific +individuals. Traversal does well here if your resources actually represent the +data objects related to your documents, because the idea of a resource +authorization is baked right into the code resolution and calling process. +Resource objects can store ACLs, which can be inherited and/or overridden by +the subresources. + +If each resource can thus generate a context-based ACL, then whenever view code +is attempting to perform a sensitive action, it can check against that ACL to +see whether the current user should be allowed to perform the action. In this +way you achieve so called "instance based" or "row level" security which is +considerably harder to model using a traditional tabular approach. :app:`Pyramid` actively supports such a scheme, and in fact if you register -your views with guard permissions and use an authorization policy, -:app:`Pyramid` can check against a resource's ACL when deciding whether or -not the view itself is available to the current user. - -In summary, there are entire classes of problems that are more easily served -by traversal and view lookup than by :term:`URL dispatch`. If your problems -don't require it, great: stick with :term:`URL dispatch`. But if you're -using :app:`Pyramid` and you ever find that you *do* need to support one of -these use cases, you'll be glad you have traversal in your toolkit. - -.. note:: It is even possible to mix and match :term:`traversal` with - :term:`URL dispatch` in the same :app:`Pyramid` application. See the +your views with guarded permissions and use an authorization policy, +:app:`Pyramid` can check against a resource's ACL when deciding whether or not +the view itself is available to the current user. + +In summary, there are entire classes of problems that are more easily served by +traversal and view lookup than by :term:`URL dispatch`. If your problems don't +require it, great, stick with :term:`URL dispatch`. But if you're using +:app:`Pyramid` and you ever find that you *do* need to support one of these use +cases, you'll be glad you have traversal in your toolkit. + +.. note:: + + It is even possible to mix and match :term:`traversal` with :term:`URL + dispatch` in the same :app:`Pyramid` application. See the :ref:`hybrid_chapter` chapter for details. diff --git a/docs/narr/paste.rst b/docs/narr/paste.rst new file mode 100644 index 000000000..0a217e6e3 --- /dev/null +++ b/docs/narr/paste.rst @@ -0,0 +1,98 @@ +.. _paste_chapter: + +PasteDeploy Configuration Files +=============================== + +Packages generated via a :term:`scaffold` make use of a system created by Ian +Bicking named :term:`PasteDeploy`. PasteDeploy defines a way to declare +:term:`WSGI` application configuration in an ``.ini`` file. + +Pyramid uses this configuration file format as input to its :term:`WSGI` server +runner ``pserve``, as well as other commands such as ``pviews``, ``pshell``, +``proutes``, and ``ptweens``. + +PasteDeploy is not a particularly integral part of Pyramid. It's possible to +create a Pyramid application which does not use PasteDeploy at all. We show a +Pyramid application that doesn't use PasteDeploy in :ref:`firstapp_chapter`. +However, all Pyramid scaffolds render PasteDeploy configuration files, to +provide new developers with a standardized way of setting deployment values, +and to provide new users with a standardized way of starting, stopping, and +debugging an application. + +This chapter is not a replacement for documentation about PasteDeploy; it only +contextualizes the use of PasteDeploy within Pyramid. For detailed +documentation, see http://pythonpaste.org/deploy/. + +PasteDeploy +----------- + +:term:`PasteDeploy` is the system that Pyramid uses to allow :term:`deployment +settings` to be specified using an ``.ini`` configuration file format. It also +allows the ``pserve`` command to work. Its configuration format provides a +convenient place to define application :term:`deployment settings` and WSGI +server settings, and its server runner allows you to stop and start a Pyramid +application easily. + +.. _pastedeploy_entry_points: + +Entry Points and PasteDeploy ``.ini`` Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the :ref:`project_narr` chapter, we breezed over the meaning of a +configuration line in the ``deployment.ini`` file. This was the ``use = +egg:MyProject`` line in the ``[app:main]`` section. We breezed over it because +it's pretty confusing and "too much information" for an introduction to the +system. We'll try to give it a bit of attention here. Let's see the config +file again: + +.. literalinclude:: MyProject/development.ini + :language: ini + :linenos: + +The line in ``[app:main]`` above that says ``use = egg:MyProject`` is actually +shorthand for a longer spelling: ``use = egg:MyProject#main``. The ``#main`` +part is omitted for brevity, as ``#main`` is a default defined by PasteDeploy. +``egg:MyProject#main`` is a string which has meaning to PasteDeploy. It points +at a :term:`setuptools` :term:`entry point` named ``main`` defined in the +``MyProject`` project. + +Take a look at the generated ``setup.py`` file for this project. + +.. literalinclude:: MyProject/setup.py + :language: python + :linenos: + +Note that ``entry_points`` is assigned a string which looks a lot like an +``.ini`` file. This string representation of an ``.ini`` file has a section +named ``[paste.app_factory]``. Within this section, there is a key named +``main`` (the entry point name) which has a value ``myproject:main``. The +*key* ``main`` is what our ``egg:MyProject#main`` value of the ``use`` section +in our config file is pointing at, although it is actually shortened to +``egg:MyProject`` there. The value represents a :term:`dotted Python name` +path, which refers to a callable in our ``myproject`` package's ``__init__.py`` +module. + +The ``egg:`` prefix in ``egg:MyProject`` indicates that this is an entry point +*URI* specifier, where the "scheme" is "egg". An "egg" is created when you run +``setup.py install`` or ``setup.py develop`` within your project. + +In English, this entry point can thus be referred to as a "PasteDeploy +application factory in the ``MyProject`` project which has the entry point +named ``main`` where the entry point refers to a ``main`` function in the +``mypackage`` module". Indeed, if you open up the ``__init__.py`` module +generated within any scaffold-generated package, you'll see a ``main`` +function. This is the function called by :term:`PasteDeploy` when the +``pserve`` command is invoked against our application. It accepts a global +configuration object and *returns* an instance of our application. + +.. _defaults_section_of_pastedeploy_file: + +``[DEFAULT]`` Section of a PasteDeploy ``.ini`` File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can add a ``[DEFAULT]`` section to your PasteDeploy ``.ini`` file. Such a +section should consist of global parameters that are shared by all the +applications, servers, and :term:`middleware` defined within the configuration +file. The values in a ``[DEFAULT]`` section will be passed to your +application's ``main`` function as ``global_config`` (see the reference to the +``main`` function in :ref:`init_py`). diff --git a/docs/narr/project-debug.png b/docs/narr/project-debug.png Binary files differnew file mode 100644 index 000000000..0a703dead --- /dev/null +++ b/docs/narr/project-debug.png diff --git a/docs/narr/project-show-toolbar.png b/docs/narr/project-show-toolbar.png Binary files differnew file mode 100644 index 000000000..89b838f64 --- /dev/null +++ b/docs/narr/project-show-toolbar.png diff --git a/docs/narr/project.png b/docs/narr/project.png Binary files differindex da5bc870b..e1afd97d4 100644 --- a/docs/narr/project.png +++ b/docs/narr/project.png diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 631412f42..81fc9acf4 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -1,166 +1,135 @@ .. _project_narr: Creating a :app:`Pyramid` Project -==================================== +================================= -As we saw in :ref:`firstapp_chapter`, it's possible to create a -:app:`Pyramid` application completely manually. However, it's usually more -convenient to use a *scaffold* to generate a basic :app:`Pyramid` -:term:`project`. +As we saw in :ref:`firstapp_chapter`, it's possible to create a :app:`Pyramid` +application completely manually. However, it's usually more convenient to use +a :term:`scaffold` to generate a basic :app:`Pyramid` :term:`project`. A project is a directory that contains at least one Python :term:`package`. You'll use a scaffold to create a project, and you'll create your application -logic within a package that lives inside the project. Even if your -application is extremely simple, it is useful to place code that drives the -application within a package, because a package is more easily extended with -new code. An application that lives inside a package can also be distributed -more easily than one which does not live within a package. +logic within a package that lives inside the project. Even if your application +is extremely simple, it is useful to place code that drives the application +within a package, because (1) a package is more easily extended with new code, +and (2) an application that lives inside a package can also be distributed more +easily than one which does not live within a package. -:app:`Pyramid` comes with a variety of scaffolds that you can use to generate -a project. Each scaffold makes different configuration assumptions about -what type of application you're trying to construct. +:app:`Pyramid` comes with a variety of scaffolds that you can use to generate a +project. Each scaffold makes different configuration assumptions about what +type of application you're trying to construct. -These scaffolds are rendered using the :term:`PasteDeploy` ``paster`` script. +These scaffolds are rendered using the ``pcreate`` command that is installed as +part of Pyramid. .. index:: single: scaffolds - single: pyramid_starter scaffold - single: pyramid_zodb scaffold - single: pyramid_alchemy scaffold - single: pyramid_routesalchemy scaffold + single: starter scaffold + single: zodb scaffold + single: alchemy scaffold .. _additional_paster_scaffolds: Scaffolds Included with :app:`Pyramid` ------------------------------------------------- +-------------------------------------- -The convenience scaffolds included with :app:`Pyramid` differ from -each other on a number of axes: +The convenience scaffolds included with :app:`Pyramid` differ from each other +on a number of axes: -- the persistence mechanism they offer (no persistence mechanism, - :term:`ZODB`, or :term:`SQLAlchemy`). +- the persistence mechanism they offer (no persistence mechanism, :term:`ZODB`, + or :term:`SQLAlchemy`) - the mechanism they use to map URLs to code (:term:`traversal` or :term:`URL - dispatch`). - -- whether or not the ``pyramid_beaker`` library is relied upon as the - sessioning implementation (as opposed to no sessioning or default - sessioning). + dispatch`) The included scaffolds are these: -``pyramid_starter`` - URL mapping via :term:`traversal` and no persistence mechanism. - -``pyramid_zodb`` - URL mapping via :term:`traversal` and persistence via :term:`ZODB`. +``starter`` + URL mapping via :term:`URL dispatch` and no persistence mechanism -``pyramid_routesalchemy`` - URL mapping via :term:`URL dispatch` and persistence via - :term:`SQLAlchemy` +``zodb`` + URL mapping via :term:`traversal` and persistence via :term:`ZODB` -``pyramid_alchemy`` - URL mapping via :term:`traversal` and persistence via - :term:`SQLAlchemy` +``alchemy`` + URL mapping via :term:`URL dispatch` and persistence via :term:`SQLAlchemy` -.. note:: At this time, each of these scaffolds uses the :term:`Chameleon` - templating system, which is incompatible with Jython. To use scaffolds to - build applications which will run on Jython, you can try the - ``pyramid_jinja2_starter`` scaffold which ships as part of the - :term:`pyramid_jinja2` package. You can also just use any above scaffold - and replace the Chameleon template it includes with a :term:`Mako` - analogue. - -Rather than use any of the above scaffolds, Pylons 1 users may feel more -comfortable installing the :term:`Akhet` development environment, which -provides a scaffold named ``akhet``. This scaffold configures a Pyramid -application in a "Pylons-esque" way, including the use of a :term:`view -handler` to map URLs to code (a handler is much like a Pylons "controller"). .. index:: single: creating a project single: project + single: pcreate .. _creating_a_project: Creating the Project -------------------- -In :ref:`installing_chapter`, you created a virtual Python environment via -the ``virtualenv`` command. To start a :app:`Pyramid` :term:`project`, use -the ``paster`` facility installed within the virtualenv. In -:ref:`installing_chapter` we called the virtualenv directory ``env``; the -following command assumes that our current working directory is that -directory. +.. seealso:: See also the output of :ref:`pcreate --help <pcreate_script>`. -We'll choose the ``pyramid_starter`` scaffold for this purpose. +In :ref:`installing_chapter`, you created a virtual Python environment via the +``venv`` command. To start a :app:`Pyramid` :term:`project`, use the +``pcreate`` command installed within the virtual environment. We'll choose the +``starter`` scaffold for this purpose. When we invoke ``pcreate``, it will +create a directory that represents our project. -.. code-block:: text +In :ref:`installing_chapter` we called the virtual environment directory +``env``. The following commands assume that our current working directory is +the ``env`` directory. - $ bin/paster create -t pyramid_starter +The below example uses the ``pcreate`` command to create a project with the +``starter`` scaffold. -The above command uses the ``paster`` command to create a project using the -``pyramid_starter`` scaffold. The ``paster create`` command creates project -from a scaffold. To use a different scaffold, such as -``pyramid_routesalchemy``, you'd just change the last argument. For example: +On UNIX: -.. code-block:: text +.. code-block:: bash - $ bin/paster create -t pyramid_routesalchemy + $ $VENV/bin/pcreate -s starter MyProject -``paster create`` will ask you a single question: the *name* of the -project. You should use a string without spaces and with only letters -in it. Here's sample output from a run of ``paster create`` for a -project we name ``MyProject``: +Or on Windows: .. code-block:: text - $ bin/paster create -t pyramid_starter - Selected and implied templates: - pyramid#pyramid_starter pyramid starter project - - Enter project name: MyProject - Variables: - egg: MyProject - package: myproject - project: MyProject - Creating template pyramid - Creating directory ./MyProject - # ... more output ... - Running /Users/chrism/projects/pyramid/bin/python setup.py egg_info - -.. note:: You can skip the interrogative question about a project - name during ``paster create`` by adding the project name to the - command line, e.g. ``paster create -t pyramid_starter MyProject``. - -.. note:: You may encounter an error when using ``paster create`` if a - dependent Python package is not installed. This will result in a traceback - ending in ``pkg_resources.DistributionNotFound: <package name>``. - Simply run ``bin/easy_install``, with the missing package name from the - error message to work around this issue. - -As a result of invoking the ``paster create`` command, a project is created -in a directory named ``MyProject``. That directory is a :term:`project` -directory. The ``setup.py`` file in that directory can be used to distribute -your application, or install your application for deployment or development. - -A :term:`PasteDeploy` ``.ini`` file named ``development.ini`` will be created -in the project directory. You will use this ``.ini`` file to configure a -server, to run your application, and to debug your application. It sports -configuration that enables an interactive debugger and settings optimized for -development. - -Another :term:`PasteDeploy` ``.ini`` file named ``production.ini`` will also -be created in the project directory. It sports configuration that disables -any interactive debugger (to prevent inappropriate access and disclosure), -and turns off a number of debugging settings. You can use this file to put -your application into production, and you can modify it to do things like -send email when an exception occurs. + > %VENV%\Scripts\pcreate -s starter MyProject + +As a result of invoking the ``pcreate`` command, a directory named +``MyProject`` is created. That directory is a :term:`project` directory. The +``setup.py`` file in that directory can be used to distribute your application, +or install your application for deployment or development. + +An ``.ini`` file named ``development.ini`` will be created in the project +directory. You will use this ``.ini`` file to configure a server, to run your +application, and to debug your application. It contains configuration that +enables an interactive debugger and settings optimized for development. + +Another ``.ini`` file named ``production.ini`` will also be created in the +project directory. It contains configuration that disables any interactive +debugger (to prevent inappropriate access and disclosure), and turns off a +number of debugging settings. You can use this file to put your application +into production. The ``MyProject`` project directory contains an additional subdirectory named -``myproject`` (note the case difference) representing a Python -:term:`package` which holds very simple :app:`Pyramid` sample code. This is -where you'll edit your application's Python code and templates. +``myproject`` (note the case difference) representing a Python :term:`package` +which holds very simple :app:`Pyramid` sample code. This is where you'll edit +your application's Python code and templates. + +We created this project within an ``env`` virtual environment directory. +However, note that this is not mandatory. The project directory can go more or +less anywhere on your filesystem. You don't need to put it in a special "web +server" directory, and you don't need to put it within a virtual environment +directory. The author uses Linux mainly, and tends to put project directories +which he creates within his ``~/projects`` directory. On Windows, it's a good +idea to put project directories within a directory that contains no space +characters, so it's wise to *avoid* a path that contains, i.e., ``My +Documents``. As a result, the author, when he uses Windows, just puts his +projects in ``C:\projects``. + +.. warning:: + + You'll need to avoid using ``pcreate`` to create a project with the same + name as a Python standard library component. In particular, this means you + should avoid using the names ``site`` or ``test``, both of which conflict + with Python standard library packages. You should also avoid using the name + ``pyramid``, which will conflict with Pyramid itself. .. index:: single: setup.py develop @@ -171,271 +140,316 @@ Installing your Newly Created Project for Development To install a newly created project for development, you should ``cd`` to the newly created project directory and use the Python interpreter from the -:term:`virtualenv` you created during :ref:`installing_chapter` to invoke the -command ``python setup.py develop`` +:term:`virtual environment` you created during :ref:`installing_chapter` to +invoke the command ``pip install -e .``, which installs the project in +development mode (``-e`` is for "editable") into the current directory (``.``). -The file named ``setup.py`` will be in the root of the paster-generated -project directory. The ``python`` you're invoking should be the one that -lives in the ``bin`` directory of your virtual Python environment. Your -terminal's current working directory *must* the the newly created project -directory. For example: +The file named ``setup.py`` will be in the root of the pcreate-generated +project directory. The ``python`` you're invoking should be the one that lives +in the ``bin`` (or ``Scripts`` on Windows) directory of your virtual Python +environment. Your terminal's current working directory *must* be the newly +created project directory. -.. code-block:: text +On UNIX: - $ ../bin/python setup.py develop +.. code-block:: bash -Elided output from a run of this command is shown below: + $ cd MyProject + $ $VENV/bin/pip install -e . -.. code-block:: text +Or on Windows: + +.. code-block:: doscon + + > cd MyProject + > %VENV%\Scripts\pip install -e . + +Elided output from a run of this command on UNIX is shown below: + +.. code-block:: bash - $ ../bin/python setup.py develop + $ cd MyProject + $ $VENV/bin/pip install -e . ... - Finished processing dependencies for MyProject==0.0 + Successfully installed Chameleon-2.24 Mako-1.0.4 MyProject \ + pyramid-chameleon-0.3 pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 This will install a :term:`distribution` representing your project into the -interpreter's library set so it can be found by ``import`` statements and by -:term:`PasteDeploy` commands such as ``paster serve`` and ``paster pshell``. +virtual environment interpreter's library set so it can be found by ``import`` +statements and by other console scripts such as ``pserve``, ``pshell``, +``proutes``, and ``pviews``. .. index:: single: running tests single: tests (running) -Running The Tests For Your Application +Running the Tests for Your Application -------------------------------------- -To run unit tests for your application, you should invoke them using the -Python interpreter from the :term:`virtualenv` you created during -:ref:`installing_chapter` (the ``python`` command that lives in the ``bin`` -directory of your virtualenv): +To run unit tests for your application, you must first install the testing +dependencies. -.. code-block:: text - - $ ../bin/python setup.py test -q - -Here's sample output from a test run: - -.. code-block:: text - - $ python setup.py test -q - running test - running egg_info - writing requirements to MyProject.egg-info/requires.txt - writing MyProject.egg-info/PKG-INFO - writing top-level names to MyProject.egg-info/top_level.txt - writing dependency_links to MyProject.egg-info/dependency_links.txt - writing entry points to MyProject.egg-info/entry_points.txt - reading manifest file 'MyProject.egg-info/SOURCES.txt' - writing manifest file 'MyProject.egg-info/SOURCES.txt' - running build_ext - .. - ---------------------------------------------------------------------- - Ran 1 test in 0.108s - - OK - -.. note:: +On UNIX: - The ``-q`` option is passed to the ``setup.py test`` command to limit the - output to a stream of dots. If you don't pass ``-q``, you'll see more - verbose test result output (which normally isn't very useful). +.. code-block:: bash -The tests themselves are found in the ``tests.py`` module in your ``paster -create`` -generated project. Within a project generated by the -``pyramid_starter`` scaffold, a single sample test exists. + $ $VENV/bin/pip install -e ".[testing]" -.. index:: - single: interactive shell - single: IPython - single: paster pshell +On Windows: -.. _interactive_shell: +.. code-block:: doscon -The Interactive Shell ---------------------- + > %VENV%\Scripts\pip install -e ".[testing]" -Once you've installed your program for development using ``setup.py -develop``, you can use an interactive Python shell to examine your -:app:`Pyramid` project's :term:`resource` and :term:`view` objects from a -Python prompt. To do so, use your virtualenv's ``paster pshell`` command. +Once the testing requirements are installed, then you can run the tests using +the ``py.test`` command that was just installed in the ``bin`` directory of +your virtual environment. -The first argument to ``pshell`` is the path to your application's ``.ini`` -file. The second is the ``app`` section name inside the ``.ini`` file which -points to *your application* as opposed to any other section within the -``.ini`` file. For example, if your application ``.ini`` file might have a -``[app:MyProject]`` section that looks like so: +On UNIX: -.. code-block:: ini - :linenos: +.. code-block:: bash - [app:MyProject] - use = egg:MyProject - reload_templates = true - debug_authorization = false - debug_notfound = false - debug_templates = true - default_locale_name = en + $ $VENV/bin/py.test myproject/tests.py -q -If so, you can use the following command to invoke a debug shell using the -name ``MyProject`` as a section name: +On Windows: -.. code-block:: text - - [chrism@vitaminf shellenv]$ ../bin/paster pshell development.ini MyProject - Python 2.4.5 (#1, Aug 29 2008, 12:27:37) - [GCC 4.0.1 (Apple Inc. build 5465)] on darwin - Type "help" for more information. "root" is the Pyramid app root object, - "registry" is the Pyramid registry object. - >>> root - <myproject.resources.MyResource object at 0x445270> - >>> registry - <Registry myproject> - >>> registry.settings['debug_notfound'] - False - >>> from myproject.views import my_view - >>> from pyramid.request import Request - >>> r = Request.blank('/') - >>> my_view(r) - {'project': 'myproject'} - -Two names are made available to the pshell user as globals: ``root`` and -``registry``. ``root`` is the the object returned by the default :term:`root -factory` in your application. ``registry`` is the :term:`application -registry` object associated with your project's application (often accessed -within view code as ``request.registry``). - -If you have `IPython <http://en.wikipedia.org/wiki/IPython>`_ installed in -the interpreter you use to invoke the ``paster`` command, the ``pshell`` -command will use an IPython interactive shell instead of a standard Python -interpreter shell. If you don't want this to happen, even if you have -IPython installed, you can pass the ``--disable-ipython`` flag to the -``pshell`` command to use a standard Python interpreter shell -unconditionally. - -.. code-block:: text +.. code-block:: doscon - [chrism@vitaminf shellenv]$ ../bin/paster pshell --disable-ipython \ - development.ini MyProject + > %VENV%\Scripts\py.test myproject\tests.py -q -You should always use a section name argument that refers to the actual -``app`` section within the Paste configuration file that points at your -:app:`Pyramid` application *without any middleware wrapping*. In particular, -a section name is inappropriate as the second argument to ``pshell`` if the -configuration section it names is a ``pipeline`` rather than an ``app``. For -example, if you have the following ``.ini`` file content: +Here's sample output from a test run on UNIX: -.. code-block:: ini - :linenos: +.. code-block:: bash - [app:MyProject] - use = egg:MyProject - reload_templates = true - debug_authorization = false - debug_notfound = false - debug_templates = true - default_locale_name = en + $ $VENV/bin/py.test myproject/tests.py -q + .. + 2 passed in 0.47 seconds - [pipeline:main] - pipeline = - egg:WebError#evalerror - MyProject +The tests themselves are found in the ``tests.py`` module in your ``pcreate`` +generated project. Within a project generated by the ``starter`` scaffold, +only two sample tests exist. -Use ``MyProject`` instead of ``main`` as the section name argument to -``pshell`` against the above ``.ini`` file (e.g. ``paster pshell -development.ini MyProject``). If you use ``main`` instead, an error will -occur. Use the most specific reference to your application within the -``.ini`` file possible as the section name argument. +.. note:: -Press ``Ctrl-D`` to exit the interactive shell (or ``Ctrl-Z`` on Windows). + The ``-q`` option is passed to the ``py.test`` command to limit the output + to a stream of dots. If you don't pass ``-q``, you'll see verbose test + result output (which normally isn't very useful). .. index:: single: running an application - single: paster serve + single: pserve single: reload single: startup - single: mod_wsgi -Running The Project Application +.. _running_the_project_application: + +Running the Project Application ------------------------------- +.. seealso:: See also the output of :ref:`pserve --help <pserve_script>`. + Once a project is installed for development, you can run the application it -represents using the ``paster serve`` command against the generated -configuration file. In our case, this file is named ``development.ini``: +represents using the ``pserve`` command against the generated configuration +file. In our case, this file is named ``development.ini``. -.. code-block:: text +On UNIX: - $ ../bin/paster serve development.ini +.. code-block:: bash -Here's sample output from a run of ``paster serve``: + $ $VENV/bin/pserve development.ini + +On Windows: .. code-block:: text - $ ../bin/paster serve development.ini - Starting server in PID 16601. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 + > %VENV%\Scripts\pserve development.ini -By default, :app:`Pyramid` applications generated from a scaffold -will listen on TCP port 6543. You can shut down a server started this way by -pressing ``Ctrl-C``. +Here's sample output from a run of ``pserve`` on UNIX: -During development, it's often useful to run ``paster serve`` using its -``--reload`` option. When ``--reload`` is passed to ``paster serve``, -changes to any Python module your project uses will cause the server to -restart. This typically makes development easier, as changes to Python code -made within a :app:`Pyramid` application is not put into effect until the -server restarts. +.. code-block:: bash -For example: + $ $VENV/bin/pserve development.ini + Starting server in PID 16208. + serving on http://127.0.0.1:6543 -.. code-block:: text +Access is restricted such that only a browser running on the same machine as +Pyramid will be able to access your Pyramid application. However, if you want +to open access to other machines on the same network, then edit the +``development.ini`` file, and replace the ``host`` value in the +``[server:main]`` section, changing it from ``127.0.0.1`` to ``0.0.0.0``. For +example: - $ ../bin/paster serve development.ini --reload - Starting subprocess with file monitor - Starting server in PID 16601. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 +.. code-block:: ini + + [server:main] + use = egg:waitress#main + host = 0.0.0.0 + port = 6543 + +Now when you use ``pserve`` to start the application, it will respond to +requests on *all* IP addresses possessed by your system, not just requests to +``localhost``. This is what the ``0.0.0.0`` in +``serving on http://0.0.0.0:6543`` means. The server will respond to requests +made to ``127.0.0.1`` and on any external IP address. For example, your system +might be configured to have an external IP address ``192.168.1.50``. If that's +the case, if you use a browser running on the same system as Pyramid, it will +be able to access the application via ``http://127.0.0.1:6543/`` as well as via +``http://192.168.1.50:6543/``. However, *other people* on other computers on +the same network will also be able to visit your Pyramid application in their +browser by visiting ``http://192.168.1.50:6543/``. + +You can change the port on which the server runs on by changing the same +portion of the ``development.ini`` file. For example, you can change the +``port = 6543`` line in the ``development.ini`` file's ``[server:main]`` +section to ``port = 8080`` to run the server on port 8080 instead of port 6543. + +You can shut down a server started this way by pressing ``Ctrl-C`` (or +``Ctrl-Break`` on Windows). + +The default server used to run your Pyramid application when a project is +created from a scaffold is named :term:`Waitress`. This server is what prints +the ``serving on...`` line when you run ``pserve``. It's a good idea to use +this server during development because it's very simple. It can also be used +for light production. Setting your application up under a different server is +not advised until you've done some development work under the default server, +particularly if you're not yet experienced with Python web development. Python +web server setup can be complex, and you should get some confidence that your +application works in a default environment before trying to optimize it or make +it "more like production". It's awfully easy to get sidetracked trying to set +up a non-default server for hours without actually starting to do any +development. One of the nice things about Python web servers is that they're +largely interchangeable, so if your application works under the default server, +it will almost certainly work under any other server in production if you +eventually choose to use a different one. Don't worry about it right now. For more detailed information about the startup process, see :ref:`startup_chapter`. For more information about environment variables and configuration file settings that influence startup and runtime behavior, see :ref:`environment_chapter`. +.. _reloading_code: + +Reloading Code +~~~~~~~~~~~~~~ + +During development, it's often useful to run ``pserve`` using its ``--reload`` +option. When ``--reload`` is passed to ``pserve``, changes to any Python +module your project uses will cause the server to restart. This typically +makes development easier, as changes to Python code made within a +:app:`Pyramid` application is not put into effect until the server restarts. + +For example, on UNIX: + +.. code-block:: text + + $ $VENV/bin/pserve development.ini --reload + Starting subprocess with file monitor + Starting server in PID 16601. + serving on http://127.0.0.1:6543 + +Now if you make a change to any of your project's ``.py`` files or ``.ini`` +files, you'll see the server restart automatically: + +.. code-block:: text + + development.ini changed; reloading... + -------------------- Restarting -------------------- + Starting server in PID 16602. + serving on http://127.0.0.1:6543 + +Changes to template files (such as ``.pt`` or ``.mak`` files) won't cause the +server to restart. Changes to template files don't require a server restart as +long as the ``pyramid.reload_templates`` setting in the ``development.ini`` +file is ``true``. Changes made to template files when this setting is true +will take effect immediately without a server restart. + +.. index:: + single: WSGI + Viewing the Application ----------------------- -Once your application is running via ``paster serve``, you may visit +Once your application is running via ``pserve``, you may visit ``http://localhost:6543/`` in your browser. You will see something in your browser like what is displayed in the following image: .. image:: project.png -This is the page shown by default when you visit an unmodified ``paster -create`` -generated ``pyramid_starter`` application in a browser. - -.. sidebar:: Using an Alternate WSGI Server - - The code generated by a :app:`Pyramid` scaffold assumes that you - will be using the ``paster serve`` command to start your application while - you do development. However, ``paster serve`` is by no means the only way - to start up and serve a :app:`Pyramid` application. As we saw in - :ref:`firstapp_chapter`, ``paster serve`` needn't be invoked at all to run - a :app:`Pyramid` application. The use of ``paster serve`` to run a - :app:`Pyramid` application is purely conventional based on the output of - its scaffold. - - Any :term:`WSGI` server is capable of running a :app:`Pyramid` - application. Some WSGI servers don't require the :term:`PasteDeploy` - framework's ``paster serve`` command to do server process management at - all. Each :term:`WSGI` server has its own documentation about how it - creates a process to run an application, and there are many of them, so we - cannot provide the details for each here. But the concepts are largely - the same, whatever server you happen to use. - - One popular production alternative to a ``paster``-invoked server is - :term:`mod_wsgi`. You can also use :term:`mod_wsgi` to serve your - :app:`Pyramid` application using the Apache web server rather than any - "pure-Python" server that is started as a result of ``paster serve``. See - :ref:`modwsgi_tutorial` for details. However, it is usually easier to - *develop* an application using a ``paster serve`` -invoked webserver, as - exception and debugging output will be sent to the console. +This is the page shown by default when you visit an unmodified ``pcreate`` +generated ``starter`` application in a browser. + +.. index:: + single: debug toolbar + +.. _debug_toolbar: + +The Debug Toolbar +~~~~~~~~~~~~~~~~~ + +.. image:: project-show-toolbar.png + +If you click on the :app:`Pyramid` logo at the top right of the page, a new +target window will open to present a debug toolbar that provides various +niceties while you're developing. This logo will float above every HTML page +served by :app:`Pyramid` while you develop an application, and allows you to +show the toolbar as necessary. + +.. image:: project-debug.png + +If you don't see the Pyramid logo on the top right of the page, it means you're +browsing from a system that does not have debugging access. By default, for +security reasons, only a browser originating from ``localhost`` (``127.0.0.1``) +can see the debug toolbar. To allow your browser on a remote system to access +the server, add a line within the ``[app:main]`` section of the +``development.ini`` file in the form ``debugtoolbar.hosts = X .X.X.X``. For +example, if your Pyramid application is running on a remote system, and you're +browsing from a host with the IP address ``192.168.1.1``, you'd add something +like this to enable the toolbar when your system contacts Pyramid: + +.. code-block:: ini + + [app:main] + # .. other settings ... + debugtoolbar.hosts = 192.168.1.1 + +For more information about what the debug toolbar allows you to do, see the +:ref:`documentation for pyramid_debugtoolbar <toolbar:overview>`. + +The debug toolbar will not be shown (and all debugging will be turned off) when +you use the ``production.ini`` file instead of the ``development.ini`` ini file +to run the application. + +You can also turn the debug toolbar off by editing ``development.ini`` and +commenting out a line. For example, instead of: + +.. code-block:: ini + :linenos: + + [app:main] + # ... elided configuration + pyramid.includes = + pyramid_debugtoolbar + +Put a hash mark at the beginning of the ``pyramid_debugtoolbar`` line: + +.. code-block:: ini + :linenos: + + [app:main] + # ... elided configuration + pyramid.includes = + # pyramid_debugtoolbar + +Then restart the application to see that the toolbar has been turned off. + +Note that if you comment out the ``pyramid_debugtoolbar`` line, the ``#`` +*must* be in the first column. If you put it anywhere else, and then attempt +to restart the application, you'll receive an error that ends something like +this: + +.. code-block:: text + + ImportError: No module named #pyramid_debugtoolbar .. index:: single: project structure @@ -443,15 +457,13 @@ create`` -generated ``pyramid_starter`` application in a browser. The Project Structure --------------------- -The ``pyramid_starter`` scaffold generated a :term:`project` (named -``MyProject``), which contains a Python :term:`package`. The package is -*also* named ``myproject``, but it's lowercased; the scaffold -generates a project which contains a package that shares its name except for -case. +The ``starter`` scaffold generated a :term:`project` (named ``MyProject``), +which contains a Python :term:`package`. The package is *also* named +``myproject``, but it's lowercased; the scaffold generates a project which +contains a package that shares its name except for case. -All :app:`Pyramid` ``paster`` -generated projects share a similar structure. -The ``MyProject`` project we've generated has the following directory -structure: +All :app:`Pyramid` ``pcreate``-generated projects share a similar structure. +The ``MyProject`` project we've generated has the following directory structure: .. code-block:: text @@ -461,49 +473,45 @@ structure: |-- MANIFEST.in |-- myproject | |-- __init__.py - | |-- resources.py | |-- static - | | |-- favicon.ico - | | |-- logo.png - | | `-- pylons.css + | | |-- pyramid-16x16.png + | | |-- pyramid.png + | | |-- theme.css + | | `-- theme.min.css | |-- templates | | `-- mytemplate.pt | |-- tests.py | `-- views.py |-- production.ini |-- README.txt - |-- setup.cfg `-- setup.py The ``MyProject`` :term:`Project` --------------------------------- -The ``MyProject`` :term:`project` directory is the distribution and -deployment wrapper for your application. It contains both the ``myproject`` +The ``MyProject`` :term:`project` directory is the distribution and deployment +wrapper for your application. It contains both the ``myproject`` :term:`package` representing your application as well as files used to describe, run, and test your application. -#. ``CHANGES.txt`` describes the changes you've made to the application. It - is conventionally written in :term:`ReStructuredText` format. +#. ``CHANGES.txt`` describes the changes you've made to the application. It is + conventionally written in :term:`ReStructuredText` format. #. ``README.txt`` describes the application in general. It is conventionally written in :term:`ReStructuredText` format. -#. ``development.ini`` is a :term:`PasteDeploy` configuration file that can - be used to execute your application during development. - -#. ``production.ini`` is a :term:`PasteDeploy` configuration file that can - be used to execute your application in a production configuration. +#. ``development.ini`` is a :term:`PasteDeploy` configuration file that can be + used to execute your application during development. -#. ``setup.cfg`` is a :term:`setuptools` configuration file used by - ``setup.py``. +#. ``production.ini`` is a :term:`PasteDeploy` configuration file that can be + used to execute your application in a production configuration. #. ``MANIFEST.in`` is a :term:`distutils` "manifest" file, naming which files should be included in a source distribution of the package when ``python setup.py sdist`` is run. -#. ``setup.py`` is the file you'll use to test and distribute your - application. It is a standard :term:`setuptools` ``setup.py`` file. +#. ``setup.py`` is the file you'll use to test and distribute your application. + It is a standard :term:`setuptools` ``setup.py`` file. .. index:: single: PasteDeploy @@ -514,127 +522,98 @@ describe, run, and test your application. ``development.ini`` ~~~~~~~~~~~~~~~~~~~ -The ``development.ini`` file is a :term:`PasteDeploy` configuration file. -Its purpose is to specify an application to run when you invoke ``paster -serve``, as well as the deployment settings provided to that application. +The ``development.ini`` file is a :term:`PasteDeploy` configuration file. Its +purpose is to specify an application to run when you invoke ``pserve``, as well +as the deployment settings provided to that application. The generated ``development.ini`` file looks like so: -.. latexbroken? - .. literalinclude:: MyProject/development.ini :language: ini :linenos: -This file contains several "sections" including ``[app:MyProject]``, -``[pipeline:main]``, and ``[server:main]``. - -The ``[app:MyProject]`` section represents configuration for your -application. This section name represents the ``MyProject`` application (and -it's an ``app`` -lication, thus ``app:MyProject``) - -The ``use`` setting is required in the ``[app:MyProject]`` section. The -``use`` setting points at a :term:`setuptools` :term:`entry point` named -``MyProject`` (the ``egg:`` prefix in ``egg:MyProject`` indicates that this -is an entry point *URI* specifier, where the "scheme" is "egg"). -``egg:MyProject`` is actually shorthand for a longer spelling: -``egg:MyProject#main``. The ``#main`` part is omitted for brevity, as it is -the default. - -.. sidebar:: ``setuptools`` Entry Points and PasteDeploy ``.ini`` Files - - This part of configuration can be confusing so let's try to clear things - up a bit. Take a look at the generated ``setup.py`` file for this - project. Note that the ``entry_point`` line in ``setup.py`` points at a - string which looks a lot like an ``.ini`` file. This string - representation of an ``.ini`` file has a section named - ``[paste.app_factory]``. Within this section, there is a key named - ``main`` (the entry point name) which has a value ``myproject:main``. The - *key* ``main`` is what our ``egg:MyProject#main`` value of the ``use`` - section in our config file is pointing at (although it is actually - shortened to ``egg:MyProject`` there). The value represents a - :term:`dotted Python name` path, which refers to a callable in our - ``myproject`` package's ``__init__.py`` module. In English, this entry - point can thus be referred to as a "Paste application factory in the - ``MyProject`` project which has the entry point named ``main`` where the - entry point refers to a ``main`` function in the ``mypackage`` module". - If indeed if you open up the ``__init__.py`` module generated within the - ``myproject`` package, you'll see a ``main`` function. This is the - function called by :term:`PasteDeploy` when the ``paster serve`` command - is invoked against our application. It accepts a global configuration - object and *returns* an instance of our application. - -The ``use`` setting is the only setting *required* in the ``[app:MyProject]`` -section unless you've changed the callable referred to by the -``egg:MyProject`` entry point to accept more arguments: other settings you -add to this section are passed as keywords arguments to the callable -represented by this entry point (``main`` in our ``__init__.py`` module). -You can provide startup-time configuration parameters to your application by -adding more settings to this section. - -The ``reload_templates`` setting in the ``[app:MyProject]`` section is a -:app:`Pyramid` -specific setting which is passed into the framework. If it -exists, and its value is ``true``, :term:`Chameleon` and :term:`Mako` -template changes will not require an application restart to be detected. See -:ref:`reload_templates_section` for more information. - -The ``debug_templates`` setting in the ``[app:MyProject]`` section is a -:app:`Pyramid` -specific setting which is passed into the framework. If it -exists, and its value is ``true``, :term:`Chameleon` template exceptions will -contained more detailed and helpful information about the error than when -this value is ``false``. See :ref:`debug_templates_section` for more -information. - -.. warning:: The ``reload_templates`` and ``debug_templates`` options should - be turned off for production applications, as template rendering is slowed - when either is turned on. - -Various other settings may exist in this section having to do with debugging -or influencing runtime behavior of a :app:`Pyramid` application. See +This file contains several sections including ``[app:main]``, +``[server:main]``, and several other sections related to logging configuration. + +The ``[app:main]`` section represents configuration for your :app:`Pyramid` +application. The ``use`` setting is the only setting required to be present in +the ``[app:main]`` section. Its default value, ``egg:MyProject``, indicates +that our MyProject project contains the application that should be served. +Other settings added to this section are passed as keyword arguments to the +function named ``main`` in our package's ``__init__.py`` module. You can +provide startup-time configuration parameters to your application by adding +more settings to this section. + +.. seealso:: See :ref:`pastedeploy_entry_points` for more information about the + meaning of the ``use = egg:MyProject`` value in this section. + +The ``pyramid.reload_templates`` setting in the ``[app:main]`` section is a +:app:`Pyramid`-specific setting which is passed into the framework. If it +exists, and its value is ``true``, supported template changes will not require +an application restart to be detected. See :ref:`reload_templates_section` for +more information. + +.. warning:: The ``pyramid.reload_templates`` option should be turned off for + production applications, as template rendering is slowed when it is turned + on. + +The ``pyramid.includes`` setting in the ``[app:main]`` section tells Pyramid to +"include" configuration from another package. In this case, the line +``pyramid.includes = pyramid_debugtoolbar`` tells Pyramid to include +configuration from the ``pyramid_debugtoolbar`` package. This turns on a +debugging panel in development mode which can be opened by clicking on the +:app:`Pyramid` logo on the top right of the screen. Including the debug +toolbar will also make it possible to interactively debug exceptions when an +error occurs. + +Various other settings may exist in this section having to do with debugging or +influencing runtime behavior of a :app:`Pyramid` application. See :ref:`environment_chapter` for more information about these settings. -``[pipeline:main]``, has the name ``main`` signifying that this is the -default 'application' (although it's actually a pipeline of middleware and an -application) run by ``paster serve`` when it is invoked against this -configuration file. The name ``main`` is a convention used by PasteDeploy -signifying that it is the default application. +The name ``main`` in ``[app:main]`` signifies that this is the default +application run by ``pserve`` when it is invoked against this configuration +file. The name ``main`` is a convention used by PasteDeploy signifying that it +is the default application. The ``[server:main]`` section of the configuration file configures a WSGI -server which listens on TCP port 6543. It is configured to listen on all -interfaces (``0.0.0.0``). The ``Paste#http`` server will create a new thread -for each request. +server which listens on TCP port 6543. It is configured to listen on localhost +only (``127.0.0.1``). -.. note:: +.. _MyProject_ini_logging: - In general, :app:`Pyramid` applications generated from scaffolds - should be threading-aware. It is not required that a :app:`Pyramid` - application be nonblocking as all application code will run in its own - thread, provided by the server you're using. +The sections after ``# logging configuration`` represent Python's standard +library :mod:`logging` module configuration for your application. These +sections are passed to the `logging module's config file configuration engine +<http://docs.python.org/howto/logging.html#configuring-logging>`_ when the +``pserve`` or ``pshell`` commands are executed. The default configuration +sends application logging output to the standard error output of your terminal. +For more information about logging configuration, see :ref:`logging_chapter`. See the :term:`PasteDeploy` documentation for more information about other types of things you can put into this ``.ini`` file, such as other -applications, :term:`middleware` and alternate :term:`WSGI` server +applications, :term:`middleware`, and alternate :term:`WSGI` server implementations. -.. note:: - - You can add a ``[DEFAULT]`` section to your ``development.ini`` file. - Such a section should consists of global parameters that are shared by all - the applications, servers and :term:`middleware` defined within the - configuration file. The values in a ``[DEFAULT]`` section will be passed - to your application's ``main`` function as ``global_config`` (see - the reference to the ``main`` function in :ref:`init_py`). +.. index:: + single: production.ini ``production.ini`` -~~~~~~~~~~~~~~~~~~~ - -The ``production.ini`` file is a :term:`PasteDeploy` configuration file with -a purpose much like that of ``development.ini``. However, it disables the -WebError interactive debugger, replacing it with a logger which outputs -exception messages to ``stderr`` by default. It also turns off template -development options such that templates are not automatically reloaded when -changed, and turns off all debugging options. You can use this file instead -of ``development.ini`` when you put your application into production. +~~~~~~~~~~~~~~~~~~ + +The ``production.ini`` file is a :term:`PasteDeploy` configuration file with a +purpose much like that of ``development.ini``. However, it disables the debug +toolbar, and filters all log messages except those above the WARN level. It +also turns off template development options such that templates are not +automatically reloaded when changed, and turns off all debugging options. This +file is appropriate to use instead of ``development.ini`` when you put your +application into production. + +It's important to use ``production.ini`` (and *not* ``development.ini``) to +benchmark your application and put it into production. ``development.ini`` +configures your system with a debug toolbar that helps development, but the +inclusion of this toolbar slows down page rendering times by over an order of +magnitude. The debug toolbar is also a potential security risk if you have it +configured incorrectly. .. index:: single: MANIFEST.in @@ -646,12 +625,41 @@ The ``MANIFEST.in`` file is a :term:`distutils` configuration file which specifies the non-Python files that should be included when a :term:`distribution` of your Pyramid project is created when you run ``python setup.py sdist``. Due to the information contained in the default -``MANIFEST.in``, an sdist of your Pyramid project will include ``.txt`` -files, ``.ini`` files, ``.rst`` files, graphics files, and template files, as -well as ``.py`` files. See +``MANIFEST.in``, an sdist of your Pyramid project will include ``.txt`` files, +``.ini`` files, ``.rst`` files, graphics files, and template files, as well as +``.py`` files. See http://docs.python.org/distutils/sourcedist.html#the-manifest-in-template for more information about the syntax and usage of ``MANIFEST.in``. +Without the presence of a ``MANIFEST.in`` file or without checking your source +code into a version control repository, ``setup.py sdist`` places only *Python +source files* (files ending with a ``.py`` extension) into tarballs generated +by ``python setup.py sdist``. This means, for example, if your project was not +checked into a setuptools-compatible source control system, and your project +directory didn't contain a ``MANIFEST.in`` file that told the ``sdist`` +machinery to include ``*.pt`` files, the ``myproject/templates/mytemplate.pt`` +file would not be included in the generated tarball. + +Projects generated by Pyramid scaffolds include a default ``MANIFEST.in`` file. +The ``MANIFEST.in`` file contains declarations which tell it to include files +like ``*.pt``, ``*.css`` and ``*.js`` in the generated tarball. If you include +files with extensions other than the files named in the project's +``MANIFEST.in`` and you don't make use of a setuptools-compatible version +control system, you'll need to edit the ``MANIFEST.in`` file and include the +statements necessary to include your new files. See +http://docs.python.org/distutils/sourcedist.html#principle for more information +about how to do this. + +You can also delete ``MANIFEST.in`` from your project and rely on a setuptools +feature which simply causes all files checked into a version control system to +be put into the generated tarball. To allow this to happen, check all the +files that you'd like to be distributed along with your application's Python +files into Subversion. After you do this, when you rerun ``setup.py sdist``, +all files checked into the version control system will be included in the +tarball. If you don't use Subversion, and instead use a different version +control system, you may need to install a setuptools add-on such as +``setuptools-git`` or ``setuptools-hg`` for this behavior to work properly. + .. index:: single: setup.py @@ -659,15 +667,16 @@ more information about the syntax and usage of ``MANIFEST.in``. ~~~~~~~~~~~~ The ``setup.py`` file is a :term:`setuptools` setup file. It is meant to be -run directly from the command line to perform a variety of functions, such as -testing your application, packaging, and distributing your application. +used to define requirements for installing dependencies for your package and +testing, as well as distributing your application. .. note:: - ``setup.py`` is the defacto standard which Python developers use to - distribute their reusable code. You can read more about ``setup.py`` files - and their usage in the `Setuptools documentation - <http://peak.telecommunity.com/DevCenter/setuptools>`_. + ``setup.py`` is the de facto standard which Python developers use to + distribute their reusable code. You can read more about ``setup.py`` files + and their usage in the `Python Packaging User Guide + <https://packaging.python.org/en/latest/>`_ and `Setuptools documentation + <http://pythonhosted.org/setuptools/>`_. Our generated ``setup.py`` looks like this: @@ -676,94 +685,46 @@ Our generated ``setup.py`` looks like this: :linenos: The ``setup.py`` file calls the setuptools ``setup`` function, which does -various things depending on the arguments passed to ``setup.py`` on the -command line. +various things depending on the arguments passed to ``pip`` on the command +line. -Within the arguments to this function call, information about your -application is kept. While it's beyond the scope of this documentation to -explain everything about setuptools setup files, we'll provide a whirlwind -tour of what exists in this file in this section. +Within the arguments to this function call, information about your application +is kept. While it's beyond the scope of this documentation to explain +everything about setuptools setup files, we'll provide a whirlwind tour of what +exists in this file in this section. Your application's name can be any string; it is specified in the ``name`` field. The version number is specified in the ``version`` value. A short -description is provided in the ``description`` field. The -``long_description`` is conventionally the content of the README and CHANGES -file appended together. The ``classifiers`` field is a list of `Trove +description is provided in the ``description`` field. The ``long_description`` +is conventionally the content of the ``README`` and ``CHANGES`` files appended +together. The ``classifiers`` field is a list of `Trove <http://pypi.python.org/pypi?%3Aaction=list_classifiers>`_ classifiers describing your application. ``author`` and ``author_email`` are text fields which probably don't need any description. ``url`` is a field that should -point at your application project's URL (if any). -``packages=find_packages()`` causes all packages within the project to be -found when packaging the application. ``include_package_data`` will include -non-Python files when the application is packaged if those files are checked -into version control. ``zip_safe`` indicates that this package is not safe -to use as a zipped egg; instead it will always unpack as a directory, which -is more convenient. ``install_requires`` and ``tests_require`` indicate that -this package depends on the ``pyramid`` package. ``test_suite`` points at -the package for our application, which means all tests found in the package -will be run when ``setup.py test`` is invoked. We examined ``entry_points`` -in our discussion of the ``development.ini`` file; this file defines the -``main`` entry point that represents our project's application. - -Usually you only need to think about the contents of the ``setup.py`` file -when distributing your application to other people, or when versioning your -application for your own use. For fun, you can try this command now: +point at your application project's URL (if any). ``packages=find_packages()`` +causes all packages within the project to be found when packaging the +application. ``include_package_data`` will include non-Python files when the +application is packaged if those files are checked into version control. +``zip_safe=False`` indicates that this package is not safe to use as a zipped +egg; instead it will always unpack as a directory, which is more convenient. +``install_requires`` indicate that this package depends on the ``pyramid`` +package. ``extras_require`` is a Python dictionary that defines what is +required to be installed for running tests. We examined ``entry_points`` in our +discussion of the ``development.ini`` file; this file defines the ``main`` +entry point that represents our project's application. + +Usually you only need to think about the contents of the ``setup.py`` file when +distributing your application to other people, when adding Python package +dependencies, or when versioning your application for your own use. For fun, +you can try this command now: .. code-block:: text - $ python setup.py sdist - -This will create a tarball of your application in a ``dist`` subdirectory -named ``MyProject-0.1.tar.gz``. You can send this tarball to other people -who want to use your application. - -.. warning:: - - Without the presence of a ``MANIFEST.in`` file or without checking your - source code into a version control repository, ``setup.py sdist`` places - only *Python source files* (files ending with a ``.py`` extension) into - tarballs generated by ``python setup.py sdist``. This means, for example, - if your project was not checked into a setuptools-compatible source - control system, and your project directory didn't contain a ``MANIFEST.in`` - file that told the ``sdist`` machinery to include ``*.pt`` files, the - ``myproject/templates/mytemplate.pt`` file would not be included in the - generated tarball. - - Projects generated by Pyramid scaffolds include a default - ``MANIFEST.in`` file. The ``MANIFEST.in`` file contains declarations - which tell it to include files like ``*.pt``, ``*.css`` and ``*.js`` in - the generated tarball. If you include files with extensions other than - the files named in the project's ``MANIFEST.in`` and you don't make use of - a setuptools-compatible version control system, you'll need to edit the - ``MANIFEST.in`` file and include the statements necessary to include your - new files. See http://docs.python.org/distutils/sourcedist.html#principle - for more information about how to do this. - - You can also delete ``MANIFEST.in`` from your project and rely on a - setuptools feature which simply causes all files checked into a version - control system to be put into the generated tarball. To allow this to - happen, check all the files that you'd like to be distributed along with - your application's Python files into Subversion. After you do this, when - you rerun ``setup.py sdist``, all files checked into the version control - system will be included in the tarball. If you don't use Subversion, and - instead use a different version control system, you may need to install a - setuptools add-on such as ``setuptools-git`` or ``setuptools-hg`` for this - behavior to work properly. - -``setup.cfg`` -~~~~~~~~~~~~~ - -The ``setup.cfg`` file is a :term:`setuptools` configuration file. It -contains various settings related to testing and internationalization: - -Our generated ``setup.cfg`` looks like this: - -.. literalinclude:: MyProject/setup.cfg - :language: guess - :linenos: + $ $VENV/bin/python setup.py sdist -The values in the default setup file allow various commonly-used -internationalization commands and testing commands to work more smoothly. +This will create a tarball of your application in a ``dist`` subdirectory named +``MyProject-0.0.tar.gz``. You can send this tarball to other people who want +to install and use your application. .. index:: single: package @@ -774,26 +735,22 @@ The ``myproject`` :term:`Package` The ``myproject`` :term:`package` lives inside the ``MyProject`` :term:`project`. It contains: -#. An ``__init__.py`` file signifies that this is a Python :term:`package`. - It also contains code that helps users run the application, including a - ``main`` function which is used as a Paste entry point. +#. An ``__init__.py`` file signifies that this is a Python :term:`package`. It + also contains code that helps users run the application, including a + ``main`` function which is used as a entry point for commands such as + ``pserve``, ``pshell``, ``pviews``, and others. -#. A ``resources.py`` module, which contains :term:`resource` code. +#. A ``templates`` directory, which contains :term:`Chameleon` (or other types + of) templates. -#. A ``templates`` directory, which contains :term:`Chameleon` (or - other types of) templates. +#. A ``tests.py`` module, which contains unit test code for the application. -#. A ``tests.py`` module, which contains unit test code for the - application. +#. A ``views.py`` module, which contains view code for the application. -#. A ``views.py`` module, which contains view code for the - application. - -These are purely conventions established by the scaffold: -:app:`Pyramid` doesn't insist that you name things in any particular way. -However, it's generally a good idea to follow Pyramid standards for naming, -so that other Pyramid developers can get up to speed quickly on your code -when you need help. +These are purely conventions established by the scaffold. :app:`Pyramid` +doesn't insist that you name things in any particular way. However, it's +generally a good idea to follow Pyramid standards for naming, so that other +Pyramid developers can get up to speed quickly on your code when you need help. .. index:: single: __init__.py @@ -815,105 +772,110 @@ also informs Python that the directory which contains it is a *package*. #. Line 1 imports the :term:`Configurator` class from :mod:`pyramid.config` that we use later. -#. Line 2 imports the ``Root`` class from :mod:`myproject.resources` that we - use later. - #. Lines 4-12 define a function named ``main`` that returns a :app:`Pyramid` WSGI application. This function is meant to be called by the - :term:`PasteDeploy` framework as a result of running ``paster serve``. + :term:`PasteDeploy` framework as a result of running ``pserve``. Within this function, application configuration is performed. - Lines 8-10 register a "default view" (a view that has no ``name`` - attribute). It is registered so that it will be found when the - :term:`context` of the request is an instance of the - :class:`myproject.resources.Root` class. The first argument to - ``add_view`` points at a Python function that does all the work for this - view, also known as a :term:`view callable`, via a :term:`dotted Python - name`. The view declaration also names a ``renderer``, which in this case - is a template that will be used to render the result of the view callable. - This particular view declaration points at - ``myproject:templates/mytemplate.pt``, which is a :term:`asset - specification` that specifies the ``mytemplate.pt`` file within the - ``templates`` directory of the ``myproject`` package. The template file - it actually points to is a :term:`Chameleon` ZPT template file. - - Line 11 registers a static view, which will serve up the files from the - ``mypackage:static`` :term:`asset specification` (the ``static`` - directory of the ``mypackage`` package). + Line 7 creates an instance of a :term:`Configurator`. + + Line 8 adds support for Chameleon templating bindings, allowing us to + specify renderers with the ``.pt`` extension. + + Line 9 registers a static view, which will serve up the files from the + ``myproject:static`` :term:`asset specification` (the ``static`` directory + of the ``myproject`` package). + + Line 10 adds a :term:`route` to the configuration. This route is later used + by a view in the ``views`` module. + + Line 11 calls ``config.scan()``, which picks up view registrations declared + elsewhere in the package (in this case, in the ``views.py`` module). Line 12 returns a :term:`WSGI` application to the caller of the function - (Paste). + (Pyramid's pserve). + +.. index:: + single: views.py ``views.py`` ~~~~~~~~~~~~ Much of the heavy lifting in a :app:`Pyramid` application is done by *view callables*. A :term:`view callable` is the main tool of a :app:`Pyramid` web -application developer; it is a bit of code which accepts a :term:`request` -and which returns a :term:`response`. +application developer; it is a bit of code which accepts a :term:`request` and +which returns a :term:`response`. .. literalinclude:: MyProject/myproject/views.py :language: python :linenos: -This bit of code was registered as the view callable within ``__init__.py`` -(via ``add_view``). ``add_view`` said that the default URL for instances -that are of the class :class:`myproject.resources.Root` should run this -:func:`myproject.views.my_view` function. +Lines 4-6 define and register a :term:`view callable` named ``my_view``. The +function named ``my_view`` is decorated with a ``view_config`` decorator (which +is processed by the ``config.scan()`` line in our ``__init__.py``). The +view_config decorator asserts that this view be found when a :term:`route` +named ``home`` is matched. In our case, because our ``__init__.py`` maps the +route named ``home`` to the URL pattern ``/``, this route will match when a +visitor visits the root URL. The view_config decorator also names a +``renderer``, which in this case is a template that will be used to render the +result of the view callable. This particular view declaration points at +``templates/mytemplate.pt``, which is an :term:`asset specification` that +specifies the ``mytemplate.pt`` file within the ``templates`` directory of the +``myproject`` package. The asset specification could have also been specified +as ``myproject:templates/mytemplate.pt``; the leading package name and colon is +optional. The template file pointed to is a :term:`Chameleon` ZPT template +file (``templates/my_template.pt``). This view callable function is handed a single piece of information: the -:term:`request`. The *request* is an instance of the :term:`WebOb` -``Request`` class representing the browser's request to our server. - -This view returns a dictionary. When this view is invoked, a -:term:`renderer` converts the dictionary returned by the view into HTML, and -returns the result as the :term:`response`. This view is configured to -invoke a renderer which uses a :term:`Chameleon` ZPT template -(``mypackage:templates/my_template.pt``, as specified in the ``__init__.py`` -file call to ``add_view``). - -See :ref:`views_which_use_a_renderer` for more information about how views, -renderers, and templates relate and cooperate. - -.. note:: Because our ``development.ini`` has a ``reload_templates = - true`` directive indicating that templates should be reloaded when - they change, you won't need to restart the application server to - see changes you make to templates. During development, this is - handy. If this directive had been ``false`` (or if the directive - did not exist), you would need to restart the application server - for each template change. For production applications, you should - set your project's ``reload_templates`` to ``false`` to increase - the speed at which templates may be rendered. +:term:`request`. The *request* is an instance of the :term:`WebOb` ``Request`` +class representing the browser's request to our server. -.. index:: - single: resources.py +This view is configured to invoke a :term:`renderer` on a template. The +dictionary the view returns (on line 6) provides the value the renderer +substitutes into the template when generating HTML. The renderer then returns +the HTML in a :term:`response`. -.. _resourcespy_project_section: +.. note:: Dictionaries provide values to :term:`template`\s. -``resources.py`` -~~~~~~~~~~~~~~~~ +.. note:: When the application is run with the scaffold's :ref:`default + development.ini <MyProject_ini>` configuration, :ref:`logging is set up + <MyProject_ini_logging>` to aid debugging. If an exception is raised, + uncaught tracebacks are displayed after the startup messages on :ref:`the + console running the server <running_the_project_application>`. Also + ``print()`` statements may be inserted into the application for debugging to + send output to this console. -The ``resources.py`` module provides the :term:`resource` data and behavior -for our application. Resources are objects which exist to provide site -structure in applications which use :term:`traversal` to map URLs to code. -We write a class named ``Root`` that provides the behavior for the root -resource. +.. note:: ``development.ini`` has a setting that controls how templates are + reloaded, ``pyramid.reload_templates``. -.. literalinclude:: MyProject/myproject/resources.py - :language: python - :linenos: + - When set to ``True`` (as in the scaffold ``development.ini``), changed + templates automatically reload without a server restart. This is + convenient while developing, but slows template rendering speed. + + - When set to ``False`` (the default value), changing templates requires a + server restart to reload them. Production applications should use + ``pyramid.reload_templates = False``. + +.. seealso:: + + See also :ref:`views_which_use_a_renderer` for more information about how + views, renderers, and templates relate and cooperate. -#. Lines 1-3 define the Root class. The Root class is a "root resource - factory" function that will be called by the :app:`Pyramid` *Router* for - each request when it wants to find the root of the resource tree. +.. seealso:: -In a "real" application, the Root object would likely not be such a simple -object. Instead, it might be an object that could access some persistent -data store, such as a database. :app:`Pyramid` doesn't make any assumption -about which sort of data storage you'll want to use, so the sample -application uses an instance of :class:`myproject.resources.Root` to -represent the root. + Pyramid can also dynamically reload changed Python files. See also + :ref:`reloading_code`. + +.. seealso:: + + See also the :ref:`debug_toolbar`, which provides interactive access to + your application's internals and, should an exception occur, allows + interactive access to traceback execution stack frames from the Python + interpreter. + +.. index:: + single: static directory ``static`` ~~~~~~~~~~ @@ -924,11 +886,11 @@ template. It includes CSS and images. ``templates/mytemplate.pt`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The single :term:`Chameleon` template exists in the project. Its contents -are too long to show here, but it displays a default page when rendered. It -is referenced by the call to ``add_view`` as the ``renderer`` attribute in -the ``__init__`` file. See :ref:`views_which_use_a_renderer` for more -information about renderers. +This is the single :term:`Chameleon` template that exists in the project. Its +contents are too long to show here, but it displays a default page when +rendered. It is referenced by the call to ``@view_config`` as the ``renderer`` +of the ``my_view`` view callable in the ``views.py`` file. See +:ref:`views_which_use_a_renderer` for more information about renderers. Templates are accessed and used by view configurations and sometimes by view functions themselves. See :ref:`templates_used_directly` and @@ -946,82 +908,120 @@ The ``tests.py`` module includes unit tests for your application. :language: python :linenos: -This sample ``tests.py`` file has a single unit test defined within it. This -test is executed when you run ``python setup.py test``. You may add more -tests here as you build your application. You are not required to write -tests to use :app:`Pyramid`, this file is simply provided as convenience and -example. +This sample ``tests.py`` file has one unit test and one functional test defined +within it. These tests are executed when you run ``py.test myproject/tests.py +-q``. You may add more tests here as you build your application. You are not +required to write tests to use :app:`Pyramid`. This file is simply provided for +convenience and example. See :ref:`testing_chapter` for more information about writing :app:`Pyramid` unit tests. +.. index:: + pair: modifying; package structure + .. _modifying_package_structure: Modifying Package Structure ----------------------------- +--------------------------- It is best practice for your application's code layout to not stray too much -from accepted Pyramid scaffold defaults. If you refrain from changing -things very much, other Pyramid coders will be able to more quickly -understand your application. However, the code layout choices made for you -by a scaffold are in no way magical or required. Despite the choices -made for you by any scaffold, you can decide to lay your code out any -way you see fit. +from accepted Pyramid scaffold defaults. If you refrain from changing things +very much, other Pyramid coders will be able to more quickly understand your +application. However, the code layout choices made for you by a scaffold are +in no way magical or required. Despite the choices made for you by any +scaffold, you can decide to lay your code out any way you see fit. For example, the configuration method named :meth:`~pyramid.config.Configurator.add_view` requires you to pass a :term:`dotted Python name` or a direct object reference as the class or -function to be used as a view. By default, the ``pyramid_starter`` scaffold -would have you add view functions to the ``views.py`` module in your -package. However, you might be more comfortable creating a ``views`` -*directory*, and adding a single file for each view. +function to be used as a view. By default, the ``starter`` scaffold would have +you add view functions to the ``views.py`` module in your package. However, you +might be more comfortable creating a ``views`` *directory*, and adding a single +file for each view. If your project package name was ``myproject`` and you wanted to arrange all your views in a Python subpackage within the ``myproject`` :term:`package` -named ``views`` instead of within a single ``views.py`` file, you might: - -- Create a ``views`` directory inside your ``mypackage`` package directory - (the same directory which holds ``views.py``). - -- *Move* the existing ``views.py`` file to a file inside the new ``views`` - directory named, say, ``blog.py``. - -- Create a file within the new ``views`` directory named ``__init__.py`` (it - can be empty, this just tells Python that the ``views`` directory is a - *package*. - -Then change the __init__.py of your myproject project (*not* the -``__init__.py`` you just created in the ``views`` directory, the one in its -parent directory). For example, from something like: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.my_view', - renderer='myproject:templates/mytemplate.pt') - -To this: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.blog.my_view', - renderer='myproject:templates/mytemplate.pt') - -You can then continue to add files to the ``views`` directory, and refer to -view classes or functions within those files via the dotted name passed as -the first argument to ``add_view``. For example, if you added a file named -``anothermodule.py`` to the ``views`` subdirectory, and added a view callable -named ``my_view`` to it: - -.. code-block:: python - :linenos: - - config.add_view('myproject.views.anothermodule.my_view', - renderer='myproject:templates/anothertemplate.pt') - -This pattern can be used to rearrage code referred to by any Pyramid API -argument which accepts a :term:`dotted Python name` or direct object -reference. - - +named ``views`` instead of within a single ``views.py`` file, you might do the +following. + +- Create a ``views`` directory inside your ``myproject`` package directory (the + same directory which holds ``views.py``). + +- Create a file within the new ``views`` directory named ``__init__.py``. (It + can be empty. This just tells Python that the ``views`` directory is a + *package*.) + +- *Move* the content from the existing ``views.py`` file to a file inside the + new ``views`` directory named, say, ``blog.py``. Because the ``templates`` + directory remains in the ``myproject`` package, the template :term:`asset + specification` values in ``blog.py`` must now be fully qualified with the + project's package name (``myproject:templates/blog.pt``). + +You can then continue to add view callable functions to the ``blog.py`` module, +but you can also add other ``.py`` files which contain view callable functions +to the ``views`` directory. As long as you use the ``@view_config`` directive +to register views in conjunction with ``config.scan()``, they will be picked up +automatically when the application is restarted. + +Using the Interactive Shell +--------------------------- + +It is possible to use the ``pshell`` command to load a Python interpreter +prompt with a similar configuration as would be loaded if you were running your +Pyramid application via ``pserve``. This can be a useful debugging tool. See +:ref:`interactive_shell` for more details. + +.. _what_is_this_pserve_thing: + +What Is This ``pserve`` Thing +----------------------------- + +The code generated by a :app:`Pyramid` scaffold assumes that you will be using +the ``pserve`` command to start your application while you do development. +``pserve`` is a command that reads a :term:`PasteDeploy` ``.ini`` file (e.g., +``development.ini``), and configures a server to serve a :app:`Pyramid` +application based on the data in the file. + +``pserve`` is by no means the only way to start up and serve a :app:`Pyramid` +application. As we saw in :ref:`firstapp_chapter`, ``pserve`` needn't be +invoked at all to run a :app:`Pyramid` application. The use of ``pserve`` to +run a :app:`Pyramid` application is purely conventional based on the output of +its scaffolding. But we strongly recommend using ``pserve`` while developing +your application because many other convenience introspection commands (such as +``pviews``, ``prequest``, ``proutes``, and others) are also implemented in +terms of configuration availability of this ``.ini`` file format. It also +configures Pyramid logging and provides the ``--reload`` switch for convenient +restarting of the server when code changes. + +.. _alternate_wsgi_server: + +Using an Alternate WSGI Server +------------------------------ + +Pyramid scaffolds generate projects which use the :term:`Waitress` WSGI server. +Waitress is a server that is suited for development and light production +usage. It's not the fastest nor the most featureful WSGI server. Instead, its +main feature is that it works on all platforms that Pyramid needs to run on, +making it a good choice as a default server from the perspective of Pyramid's +developers. + +Any WSGI server is capable of running a :app:`Pyramid` application. But we +suggest you stick with the default server for development, and that you wait to +investigate other server options until you're ready to deploy your application +to production. Unless for some reason you need to develop on a non-local +system, investigating alternate server options is usually a distraction until +you're ready to deploy. But we recommend developing using the default +configuration on a local system that you have complete control over; it will +provide the best development experience. + +One popular production alternative to the default Waitress server is +:term:`mod_wsgi`. You can use mod_wsgi to serve your :app:`Pyramid` application +using the Apache web server rather than any "pure-Python" server like Waitress. +It is fast and featureful. See :ref:`modwsgi_tutorial` for details. + +Another good production alternative is :term:`Green Unicorn` (aka +``gunicorn``). It's faster than Waitress and slightly easier to configure than +mod_wsgi, although it depends, in its default configuration, on having a +buffering HTTP proxy in front of it. It does not, as of this writing, work on +Windows. diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index b284fe73f..50e85813a 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -3,10 +3,10 @@ Renderers ========= -A view needn't *always* return a :term:`Response` object. If a view -happens to return something which does not implement the Pyramid -Response interface, :app:`Pyramid` will attempt to use a -:term:`renderer` to construct a response. For example: +A view callable needn't *always* return a :term:`Response` object. If a view +happens to return something which does not implement the Pyramid Response +interface, :app:`Pyramid` will attempt to use a :term:`renderer` to construct a +response. For example: .. code-block:: python :linenos: @@ -17,30 +17,26 @@ Response interface, :app:`Pyramid` will attempt to use a def hello_world(request): return {'content':'Hello!'} -The above example returns a *dictionary* from the view callable. A -dictionary does not implement the Pyramid response interface, so you might -believe that this example would fail. However, since a ``renderer`` is -associated with the view callable through its :term:`view configuration` (in -this case, using a ``renderer`` argument passed to -:func:`~pyramid.view.view_config`), if the view does *not* return a Response -object, the renderer will attempt to convert the result of the view to a -response on the developer's behalf. +The above example returns a *dictionary* from the view callable. A dictionary +does not implement the Pyramid response interface, so you might believe that +this example would fail. However, since a ``renderer`` is associated with the +view callable through its :term:`view configuration` (in this case, using a +``renderer`` argument passed to :func:`~pyramid.view.view_config`), if the view +does *not* return a Response object, the renderer will attempt to convert the +result of the view to a response on the developer's behalf. -Of course, if no renderer is associated with a view's configuration, -returning anything except an object which implements the Response interface -will result in an error. And, if a renderer *is* used, whatever is returned -by the view must be compatible with the particular kind of renderer used, or -an error may occur during view invocation. +Of course, if no renderer is associated with a view's configuration, returning +anything except an object which implements the Response interface will result +in an error. And, if a renderer *is* used, whatever is returned by the view +must be compatible with the particular kind of renderer used, or an error may +occur during view invocation. -One exception exists: it is *always* OK to return a Response object, even -when a ``renderer`` is configured. If a view callable returns a response -object from a view that is configured with a renderer, the renderer is -bypassed entirely. - -Various types of renderers exist, including serialization renderers -and renderers which use templating systems. See also -:ref:`views_which_use_a_renderer`. +One exception exists: it is *always* OK to return a Response object, even when +a ``renderer`` is configured. In such cases, the renderer is bypassed +entirely. +Various types of renderers exist, including serialization renderers and +renderers which use templating systems. .. index:: single: renderer @@ -51,19 +47,22 @@ and renderers which use templating systems. See also Writing View Callables Which Use a Renderer ------------------------------------------- -As we've seen, view callables needn't always return a Response object. -Instead, they may return an arbitrary Python object, with the expectation -that a :term:`renderer` will convert that object into a response instance on -your behalf. Some renderers use a templating system; other renderers use -object serialization techniques. - -View configuration can vary the renderer associated with a view callable via -the ``renderer`` attribute. For example, this call to +As we've seen, a view callable needn't always return a Response object. +Instead, it may return an arbitrary Python object, with the expectation that a +:term:`renderer` will convert that object into a response instance on your +behalf. Some renderers use a templating system, while other renderers use +object serialization techniques. In practice, renderers obtain application +data values from Python dictionaries so, in practice, view callables which use +renderers return Python dictionaries. + +View callables can :ref:`explicitly call <example_render_to_response_call>` +renderers, but typically don't. Instead view configuration declares the +renderer used to render a view callable's results. This is done with the +``renderer`` attribute. For example, this call to :meth:`~pyramid.config.Configurator.add_view` associates the ``json`` renderer with a view callable: .. code-block:: python - :linenos: config.add_view('myproject.views.my_view', renderer='json') @@ -71,31 +70,54 @@ When this configuration is added to an application, the ``myproject.views.my_view`` view callable will now use a ``json`` renderer, which renders view return values to a :term:`JSON` response serialization. -Other built-in renderers include renderers which use the :term:`Chameleon` -templating language to render a dictionary to a response. +Pyramid defines several :ref:`built_in_renderers`, and additional renderers can +be added by developers to the system as necessary. See +:ref:`adding_and_overriding_renderers`. + +Views which use a renderer and return a non-Response value can vary non-body +response attributes (such as headers and the HTTP status code) by attaching a +property to the ``request.response`` attribute. See +:ref:`request_response_attr`. + +As already mentioned, if the :term:`view callable` associated with a +:term:`view configuration` returns a Response object (or its instance), any +renderer associated with the view configuration is ignored, and the response is +passed back to :app:`Pyramid` unchanged. For example: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + @view_config(renderer='json') + def view(request): + return Response('OK') # json renderer avoided -If the :term:`view callable` associated with a :term:`view configuration` -returns a Response object directly (an object with the attributes ``status``, -``headerlist`` and ``app_iter``), any renderer associated with the view -configuration is ignored, and the response is passed back to :app:`Pyramid` -unchanged. For example, if your view callable returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class as a response, no renderer -will be employed. +Likewise for an :term:`HTTP exception` response: .. code-block:: python :linenos: from pyramid.httpexceptions import HTTPFound + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return HTTPFound(location='http://example.com') # any renderer avoided + return HTTPFound(location='http://example.com') # json renderer avoided -Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching a property to the -``request.response`` attribute See :ref:`request_response_attr`. +You can of course also return the ``request.response`` attribute instead to +avoid rendering: -Additional renderers can be added by developers to the system as necessary -(see :ref:`adding_and_overriding_renderers`). +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='json') + def view(request): + request.response.body = 'OK' + return request.response # json renderer avoided .. index:: single: renderers (built-in) @@ -103,45 +125,46 @@ Additional renderers can be added by developers to the system as necessary .. _built_in_renderers: -Built-In Renderers +Built-in Renderers ------------------ Several built-in renderers exist in :app:`Pyramid`. These renderers can be used in the ``renderer`` attribute of view configurations. +.. note:: + + Bindings for officially supported templating languages can be found at + :ref:`available_template_system_bindings`. + .. index:: pair: renderer; string ``string``: String Renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``string`` renderer is a renderer which renders a view callable result to -a string. If a view callable returns a non-Response object, and the -``string`` renderer is associated in that view's configuration, the result -will be to run the object through the Python ``str`` function to generate a -string. Note that if a Unicode object is returned by the view callable, it -is not ``str()`` -ified. +The ``string`` renderer renders a view callable result to a string. If a view +callable returns a non-Response object, and the ``string`` renderer is +associated in that view's configuration, the result will be to run the object +through the Python ``str`` function to generate a string. Note that if a +Unicode object is returned by the view callable, it is not ``str()``-ified. Here's an example of a view that returns a dictionary. If the ``string`` -renderer is specified in the configuration for this view, the view will -render the returned dictionary to the ``str()`` representation of the -dictionary: +renderer is specified in the configuration for this view, the view will render +the returned dictionary to the ``str()`` representation of the dictionary: .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='string') def hello_world(request): return {'content':'Hello!'} -The body of the response returned by such a view will be a string -representing the ``str()`` serialization of the return value: +The body of the response returned by such a view will be a string representing +the ``str()`` serialization of the return value: .. code-block:: python - :linenos: {'content': 'Hello!'} @@ -152,184 +175,211 @@ using the API of the ``request.response`` attribute. See .. index:: pair: renderer; JSON -``json``: JSON Renderer -~~~~~~~~~~~~~~~~~~~~~~~ +.. _json_renderer: -The ``json`` renderer renders view callable results to :term:`JSON`. It -passes the return value through the ``json.dumps`` standard library function, -and wraps the result in a response object. It also sets the response +JSON Renderer +~~~~~~~~~~~~~ + +The ``json`` renderer renders view callable results to :term:`JSON`. By +default, it passes the return value through the ``json.dumps`` standard library +function, and wraps the result in a response object. It also sets the response content-type to ``application/json``. Here's an example of a view that returns a dictionary. Since the ``json`` -renderer is specified in the configuration for this view, the view will -render the returned dictionary to a JSON serialization: +renderer is specified in the configuration for this view, the view will render +the returned dictionary to a JSON serialization: .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='json') def hello_world(request): return {'content':'Hello!'} -The body of the response returned by such a view will be a string -representing the JSON serialization of the return value: +The body of the response returned by such a view will be a string representing +the JSON serialization of the return value: .. code-block:: python - :linenos: - '{"content": "Hello!"}' + {"content": "Hello!"} The return value needn't be a dictionary, but the return value must contain -values serializable by :func:`json.dumps`. +values serializable by the configured serializer (by default ``json.dumps``). You can configure a view to use the JSON renderer by naming ``json`` as the -``renderer`` argument of a view configuration, e.g. by using +``renderer`` argument of a view configuration, e.g., by using :meth:`~pyramid.config.Configurator.add_view`: .. code-block:: python :linenos: config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='json') - + name='hello', + context='myproject.resources.Hello', + renderer='json') Views which use the JSON renderer can vary non-body response attributes by -using the api of the ``request.response`` attribute. See +using the API of the ``request.response`` attribute. See :ref:`request_response_attr`. -.. index:: - pair: renderer; chameleon - -.. _chameleon_template_renderers: - -``*.pt`` or ``*.txt``: Chameleon Template Renderers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Two built-in renderers exist for :term:`Chameleon` templates. - -If the ``renderer`` attribute of a view configuration is an absolute path, a -relative path or :term:`asset specification` which has a final path element -with a filename extension of ``.pt``, the Chameleon ZPT renderer is used. -See :ref:`chameleon_zpt_templates` for more information about ZPT templates. - -If the ``renderer`` attribute of a view configuration is an absolute path or -a :term:`asset specification` which has a final path element with a filename -extension of ``.txt``, the :term:`Chameleon` text renderer is used. See -:ref:`chameleon_text_templates` for more information about Chameleon text -templates. - -The behavior of these renderers is the same, except for the engine -used to render the template. - -When a ``renderer`` attribute that names a template path or :term:`asset -specification` (e.g. ``myproject:templates/foo.pt`` or -``myproject:templates/foo.txt``) is used, the view must return a -:term:`Response` object or a Python *dictionary*. If the view callable with -an associated template returns a Python dictionary, the named template will -be passed the dictionary as its keyword arguments, and the template renderer -implementation will return the resulting rendered template in a response to -the user. If the view callable returns anything but a Response object or a -dictionary, an error will be raised. - -Before passing keywords to the template, the keyword arguments derived from -the dictionary returned by the view are augmented. The callable object -- -whatever object was used to define the view -- will be automatically -inserted into the set of keyword arguments passed to the template as the -``view`` keyword. If the view callable was a class, the ``view`` keyword -will be an instance of that class. Also inserted into the keywords passed to -the template are ``renderer_name`` (the string used in the ``renderer`` -attribute of the directive), ``renderer_info`` (an object containing -renderer-related information), ``context`` (the context resource of the view -used to render the template), and ``request`` (the request passed to the view -used to render the template). - -Here's an example view configuration which uses a Chameleon ZPT renderer: +.. _json_serializing_custom_objects: + +Serializing Custom Objects +++++++++++++++++++++++++++ + +Some objects are not, by default, JSON-serializable (such as datetimes and +other arbitrary Python objects). You can, however, register code that makes +non-serializable objects serializable in two ways: + +- Define a ``__json__`` method on objects in your application. + +- For objects you don't "own", you can register a JSON renderer that knows + about an *adapter* for that kind of object. + +Using a Custom ``__json__`` Method +********************************** + +Custom objects can be made easily JSON-serializable in Pyramid by defining a +``__json__`` method on the object's class. This method should return values +natively JSON-serializable (such as ints, lists, dictionaries, strings, and so +forth). It should accept a single additional argument, ``request``, which will +be the active request object at render time. .. code-block:: python :linenos: - # config is an instance of pyramid.config.Configurator + from pyramid.view import view_config + + class MyObject(object): + def __init__(self, x): + self.x = x + + def __json__(self, request): + return {'x':self.x} + + @view_config(renderer='json') + def objects(request): + return [MyObject(1), MyObject(2)] + + # the JSON value returned by ``objects`` will be: + # [{"x": 1}, {"x": 2}] - config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='myproject:templates/foo.pt') +Using the ``add_adapter`` Method of a Custom JSON Renderer +********************************************************** -Here's an example view configuration which uses a Chameleon text renderer: +If you aren't the author of the objects being serialized, it won't be possible +(or at least not reasonable) to add a custom ``__json__`` method to their +classes in order to influence serialization. If the object passed to the +renderer is not a serializable type and has no ``__json__`` method, usually a +:exc:`TypeError` will be raised during serialization. You can change this +behavior by creating a custom JSON renderer and adding adapters to handle +custom types. The renderer will attempt to adapt non-serializable objects using +the registered adapters. A short example follows: .. code-block:: python :linenos: - config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='myproject:templates/foo.txt') + from pyramid.renderers import JSON -Views which use a Chameleon renderer can vary response attributes by using -the API of the ``request.response`` attribute. See -:ref:`request_response_attr`. + if __name__ == '__main__': + config = Configurator() + json_renderer = JSON() + def datetime_adapter(obj, request): + return obj.isoformat() + json_renderer.add_adapter(datetime.datetime, datetime_adapter) + config.add_renderer('json', json_renderer) + +The ``add_adapter`` method should accept two arguments: the *class* of the +object that you want this adapter to run for (in the example above, +``datetime.datetime``), and the adapter itself. + +The adapter should be a callable. It should accept two arguments: the object +needing to be serialized and ``request``, which will be the current request +object at render time. The adapter should raise a :exc:`TypeError` if it can't +determine what to do with the object. + +See :class:`pyramid.renderers.JSON` and :ref:`adding_and_overriding_renderers` +for more information. + +.. versionadded:: 1.4 + Serializing custom objects. .. index:: - pair: renderer; mako + pair: renderer; JSONP -.. _mako_template_renderers: +.. _jsonp_renderer: -``*.mak`` or ``*.mako``: Mako Template Renderer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +JSONP Renderer +~~~~~~~~~~~~~~ -The ``Mako`` template renderer renders views using a Mako template. When -used, the view must return a Response object or a Python *dictionary*. The -dictionary items will then be used in the global template space. If the view -callable returns anything but a Response object or a dictionary, an error -will be raised. +.. versionadded:: 1.1 -When using a ``renderer`` argument to a :term:`view configuration` to specify -a Mako template, the value of the ``renderer`` may be a path relative to the -``mako.directories`` setting (e.g. ``some/template.mak``) or, alternately, -it may be a :term:`asset specification` -(e.g. ``apackage:templates/sometemplate.mak``). Mako templates may -internally inherit other Mako templates using a relative filename or a -:term:`asset specification` as desired. +:class:`pyramid.renderers.JSONP` is a `JSONP +<http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper which implements +a hybrid JSON/JSONP renderer. JSONP is useful for making cross-domain AJAX +requests. -Here's an example view configuration which uses a relative path: +Unlike other renderers, a JSONP renderer needs to be configured at startup time +"by hand". Configure a JSONP renderer using the +:meth:`pyramid.config.Configurator.add_renderer` method: .. code-block:: python - :linenos: - # config is an instance of pyramid.config.Configurator + from pyramid.config import Configurator + from pyramid.renderers import JSONP - config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='foo.mak') + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback')) -It's important to note that in Mako's case, the 'relative' path name -``foo.mak`` above is not relative to the package, but is relative to the -directory (or directories) configured for Mako via the ``mako.directories`` -configuration file setting. - -The renderer can also be provided in :term:`asset specification` -format. Here's an example view configuration which uses one: +Once this renderer is registered via +:meth:`~pyramid.config.Configurator.add_renderer` as above, you can use +``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or +:meth:`pyramid.config.Configurator.add_view`: .. code-block:: python - :linenos: - config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='mypackage:templates/foo.mak') + from pyramid.view import view_config + + @view_config(renderer='jsonp') + def myview(request): + return {'greeting':'Hello world'} + +When a view is called that uses a JSONP renderer: + +- If there is a parameter in the request's HTTP query string (aka + ``request.GET``) that matches the ``param_name`` of the registered JSONP + renderer (by default, ``callback``), the renderer will return a JSONP + response. + +- If there is no callback parameter in the request's query string, the renderer + will return a "plain" JSON response. + +Javscript library AJAX functionality will help you make JSONP requests. +For example, JQuery has a `getJSON function +<http://api.jquery.com/jQuery.getJSON/>`_, and has equivalent (but more +complicated) functionality in its `ajax function +<http://api.jquery.com/jQuery.ajax/>`_. + +For example (JavaScript): -The above configuration will use the file named ``foo.mak`` in the -``templates`` directory of the ``mypackage`` package. +.. code-block:: javascript -The ``Mako`` template renderer can take additional arguments beyond the -standard ``reload_templates`` setting, see the :ref:`environment_chapter` for -additional :ref:`mako_template_renderer_settings`. + var api_url = 'http://api.geonames.org/timezoneJSON' + + '?lat=38.301733840000004' + + '&lng=-77.45869621' + + '&username=fred' + + '&callback=?'; + jqhxr = $.getJSON(api_url); + +The string ``callback=?`` above in the ``url`` param to the JQuery ``getJSON`` +function indicates to jQuery that the query should be made as a JSONP request; +the ``callback`` parameter will be automatically filled in for you and used. + +The same custom-object serialization scheme defined used for a "normal" JSON +renderer in :ref:`json_serializing_custom_objects` can be used when passing +values to a JSONP renderer too. .. index:: single: response headers (from a renderer) @@ -344,10 +394,9 @@ Before a response constructed by a :term:`renderer` is returned to :app:`Pyramid`, several attributes of the request are examined which have the potential to influence response behavior. -View callables that don't directly return a response should use the API of -the :class:`pyramid.response.Response` attribute available as -``request.response`` during their execution, to influence associated response -behavior. +View callables that don't directly return a response should use the API of the +:class:`pyramid.response.Response` attribute, available as ``request.response`` +during their execution, to influence associated response behavior. For example, if you need to change the response status from within a view callable that uses a renderer, assign the ``status`` attribute to the @@ -363,48 +412,34 @@ callable that uses a renderer, assign the ``status`` attribute to the request.response.status = '404 Not Found' return {'URL':request.URL} -For more information on attributes of the request, see the API documentation -in :ref:`request_module`. For more information on the API of -``request.response``, see :class:`pyramid.response.Response`. - -.. _response_prefixed_attrs: - -Deprecated Mechanism to Vary Attributes of Rendered Responses -------------------------------------------------------------- +Note that mutations of ``request.response`` in views which return a Response +object directly will have no effect unless the response object returned *is* +``request.response``. For example, the following example calls +``request.response.set_cookie``, but this call will have no effect because a +different Response object is returned. -.. warning:: This section describes behavior deprecated in Pyramid 1.1. +.. code-block:: python + :linenos: -In previous releases of Pyramid (1.0 and before), the ``request.response`` -attribute did not exist. Instead, Pyramid required users to set special -``response_`` -prefixed attributes of the request to influence response -behavior. As of Pyramid 1.1, those request attributes are deprecated and -their use will cause a deprecation warning to be issued when used. Until -their existence is removed completely, we document them below, for benefit of -people with older code bases. + from pyramid.response import Response -``response_content_type`` - Defines the content-type of the resulting response, - e.g. ``text/xml``. + def view(request): + request.response.set_cookie('abc', '123') # this has no effect + return Response('OK') # because we're returning a different response -``response_headerlist`` - A sequence of tuples describing header values that should be set in the - response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header', 'foo')]``. +If you mutate ``request.response`` and you'd like the mutations to have an +effect, you must return ``request.response``: -``response_status`` - A WSGI-style status code (e.g. ``200 OK``) describing the status of the - response. +.. code-block:: python + :linenos: -``response_charset`` - The character set (e.g. ``UTF-8``) of the response. + def view(request): + request.response.set_cookie('abc', '123') + return request.response -``response_cache_for`` - A value in seconds which will influence ``Cache-Control`` and ``Expires`` - headers in the returned response. The same can also be achieved by - returning various values in the ``response_headerlist``, this is purely a - convenience. - -.. index:: - single: renderer (adding) +For more information on attributes of the request, see the API documentation in +:ref:`request_module`. For more information on the API of +``request.response``, see :attr:`pyramid.request.Request.response`. .. _adding_and_overriding_renderers: @@ -423,7 +458,6 @@ For example, to add a renderer which renders views which have a ``renderer`` attribute that is a path that ends in ``.jinja2``: .. code-block:: python - :linenos: config.add_renderer('.jinja2', 'mypackage.MyJinja2Renderer') @@ -431,6 +465,9 @@ The first argument is the renderer name. The second argument is a reference to an implementation of a :term:`renderer factory` or a :term:`dotted Python name` referring to such an object. +.. index:: + pair: renderer; adding + .. _adding_a_renderer: Adding a New Renderer @@ -439,8 +476,10 @@ Adding a New Renderer You may add a new renderer by creating and registering a :term:`renderer factory`. -A renderer factory implementation is typically a class with the -following interface: +A renderer factory implementation should conform to the +:class:`pyramid.interfaces.IRendererFactory` interface. It should be capable of +creating an object that conforms to the :class:`pyramid.interfaces.IRenderer` +interface. A typical class that follows this setup is as follows: .. code-block:: python :linenos: @@ -460,42 +499,38 @@ following interface: the result (a string or unicode object). The value is the return value of a view. The system value is a dictionary containing available system values - (e.g. view, context, and request). """ + (e.g., view, context, and request). """ The formal interface definition of the ``info`` object passed to a renderer factory constructor is available as :class:`pyramid.interfaces.IRendererInfo`. There are essentially two different kinds of renderer factories: -- A renderer factory which expects to accept an :term:`asset - specification`, or an absolute path, as the ``name`` attribute of the - ``info`` object fed to its constructor. These renderer factories are - registered with a ``name`` value that begins with a dot (``.``). These - types of renderer factories usually relate to a file on the filesystem, - such as a template. +- A renderer factory which expects to accept an :term:`asset specification`, or + an absolute path, as the ``name`` attribute of the ``info`` object fed to its + constructor. These renderer factories are registered with a ``name`` value + that begins with a dot (``.``). These types of renderer factories usually + relate to a file on the filesystem, such as a template. -- A renderer factory which expects to accept a token that does not represent - a filesystem path or an asset specification in the ``name`` - attribute of the ``info`` object fed to its constructor. These renderer - factories are registered with a ``name`` value that does not begin with a - dot. These renderer factories are typically object serializers. +- A renderer factory which expects to accept a token that does not represent a + filesystem path or an asset specification in the ``name`` attribute of the + ``info`` object fed to its constructor. These renderer factories are + registered with a ``name`` value that does not begin with a dot. These + renderer factories are typically object serializers. .. sidebar:: Asset Specifications - An asset specification is a colon-delimited identifier for an - :term:`asset`. The colon separates a Python :term:`package` - name from a package subpath. For example, the asset - specification ``my.package:static/baz.css`` identifies the file named - ``baz.css`` in the ``static`` subdirectory of the ``my.package`` Python - :term:`package`. + An asset specification is a colon-delimited identifier for an :term:`asset`. + The colon separates a Python :term:`package` name from a package subpath. + For example, the asset specification ``my.package:static/baz.css`` + identifies the file named ``baz.css`` in the ``static`` subdirectory of the + ``my.package`` Python :term:`package`. Here's an example of the registration of a simple renderer factory via -:meth:`~pyramid.config.Configurator.add_renderer`: +:meth:`~pyramid.config.Configurator.add_renderer`, where ``config`` is an +instance of :meth:`pyramid.config.Configurator`: .. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator config.add_renderer(name='amf', factory='my.package.MyAMFRenderer') @@ -514,26 +549,23 @@ renderer by specifying ``amf`` in the ``renderer`` attribute of a def myview(request): return {'Hello':'world'} -At startup time, when a :term:`view configuration` is encountered, which -has a ``name`` attribute that does not contain a dot, the full ``name`` -value is used to construct a renderer from the associated renderer -factory. In this case, the view configuration will create an instance -of an ``MyAMFRenderer`` for each view configuration which includes ``amf`` -as its renderer value. The ``name`` passed to the ``MyAMFRenderer`` -constructor will always be ``amf``. +At startup time, when a :term:`view configuration` is encountered which has a +``name`` attribute that does not contain a dot, the full ``name`` value is used +to construct a renderer from the associated renderer factory. In this case, +the view configuration will create an instance of an ``MyAMFRenderer`` for each +view configuration which includes ``amf`` as its renderer value. The ``name`` +passed to the ``MyAMFRenderer`` constructor will always be ``amf``. -Here's an example of the registration of a more complicated renderer -factory, which expects to be passed a filesystem path: +Here's an example of the registration of a more complicated renderer factory, +which expects to be passed a filesystem path: .. code-block:: python - :linenos: - config.add_renderer(name='.jinja2', - factory='my.package.MyJinja2Renderer') + config.add_renderer(name='.jinja2', factory='my.package.MyJinja2Renderer') Adding the above code to your application startup will allow you to use the ``my.package.MyJinja2Renderer`` renderer factory implementation in view -configurations by referring to any ``renderer`` which *ends in* ``.jinja`` in +configurations by referring to any ``renderer`` which *ends in* ``.jinja2`` in the ``renderer`` attribute of a :term:`view configuration`: .. code-block:: python @@ -545,81 +577,76 @@ the ``renderer`` attribute of a :term:`view configuration`: def myview(request): return {'Hello':'world'} -When a :term:`view configuration` is encountered at startup time, which -has a ``name`` attribute that does contain a dot, the value of the name -attribute is split on its final dot. The second element of the split is -typically the filename extension. This extension is used to look up a -renderer factory for the configured view. Then the value of -``renderer`` is passed to the factory to create a renderer for the view. -In this case, the view configuration will create an instance of a -``MyJinja2Renderer`` for each view configuration which includes anything -ending with ``.jinja2`` in its ``renderer`` value. The ``name`` passed -to the ``MyJinja2Renderer`` constructor will be the full value that was -set as ``renderer=`` in the view configuration. +When a :term:`view configuration` is encountered at startup time which has a +``name`` attribute that does contain a dot, the value of the name attribute is +split on its final dot. The second element of the split is typically the +filename extension. This extension is used to look up a renderer factory for +the configured view. Then the value of ``renderer`` is passed to the factory +to create a renderer for the view. In this case, the view configuration will +create an instance of a ``MyJinja2Renderer`` for each view configuration which +includes anything ending with ``.jinja2`` in its ``renderer`` value. The +``name`` passed to the ``MyJinja2Renderer`` constructor will be the full value +that was set as ``renderer=`` in the view configuration. -Changing an Existing Renderer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adding a Default Renderer +~~~~~~~~~~~~~~~~~~~~~~~~~ -You can associate more than one filename extension with the same existing -renderer implementation as necessary if you need to use a different file -extension for the same kinds of templates. For example, to associate the -``.zpt`` extension with the Chameleon ZPT renderer factory, use the -:meth:`pyramid.config.Configurator.add_renderer` method: +To associate a *default* renderer with *all* view configurations (even ones +which do not possess a ``renderer`` attribute), pass ``None`` as the ``name`` +attribute to the renderer tag: .. code-block:: python - :linenos: - config.add_renderer('.zpt', 'pyramid.chameleon_zpt.renderer_factory') - -After you do this, :app:`Pyramid` will treat templates ending in both the -``.pt`` and ``.zpt`` filename extensions as Chameleon ZPT templates. + config.add_renderer(None, 'mypackage.json_renderer_factory') -To change the default mapping in which files with a ``.pt`` extension are -rendered via a Chameleon ZPT page template renderer, use a variation on the -following in your application's startup code: +.. index:: + pair: renderer; changing -.. code-block:: python - :linenos: +Changing an Existing Renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - config.add_renderer('.pt', 'mypackage.pt_renderer') +Pyramid supports overriding almost every aspect of its setup through its +:ref:`Conflict Resolution <automatic_conflict_resolution>` mechanism. This +means that, in most cases, overriding a renderer is as simple as using the +:meth:`pyramid.config.Configurator.add_renderer` method to redefine the +template extension. For example, if you would like to override the ``json`` +renderer to specify a new renderer, you could do the following: -After you do this, the :term:`renderer factory` in -``mypackage.pt_renderer`` will be used to render templates which end -in ``.pt``, replacing the default Chameleon ZPT renderer. +.. code-block:: python -To associate a *default* renderer with *all* view configurations (even -ones which do not possess a ``renderer`` attribute), pass ``None`` as -the ``name`` attribute to the renderer tag: + json_renderer = pyramid.renderers.JSON() + config.add_renderer('json', json_renderer) -.. code-block:: python - :linenos: +After doing this, any views registered with the ``json`` renderer will use the +new renderer. - config.add_renderer(None, 'mypackage.json_renderer_factory') +.. index:: + pair: renderer; overriding at runtime -Overriding A Renderer At Runtime +Overriding a Renderer at Runtime -------------------------------- .. warning:: This is an advanced feature, not typically used by "civilians". In some circumstances, it is necessary to instruct the system to ignore the static renderer declaration provided by the developer in view configuration, -replacing the renderer with another *after a request starts*. For example, -an "omnipresent" XML-RPC implementation that detects that the request is from -an XML-RPC client might override a view configuration statement made by the -user instructing the view to use a template renderer with one that uses an -XML-RPC renderer. This renderer would produce an XML-RPC representation of -the data returned by an arbitrary view callable. +replacing the renderer with another *after a request starts*. For example, an +"omnipresent" XML-RPC implementation that detects that the request is from an +XML-RPC client might override a view configuration statement made by the user +instructing the view to use a template renderer with one that uses an XML-RPC +renderer. This renderer would produce an XML-RPC representation of the data +returned by an arbitrary view callable. To use this feature, create a :class:`~pyramid.events.NewRequest` :term:`subscriber` which sniffs at the request data and which conditionally -sets an ``override_renderer`` attribute on the request itself, which is the -*name* of a registered renderer. For example: +sets an ``override_renderer`` attribute on the request itself, which in turn is +the *name* of a registered renderer. For example: .. code-block:: python :linenos: - from pyramid.event import subscriber - from pyramid.event import NewRequest + from pyramid.events import subscriber + from pyramid.events import NewRequest @subscriber(NewRequest) def set_xmlrpc_params(event): @@ -634,6 +661,6 @@ sets an ``override_renderer`` attribute on the request itself, which is the request.override_renderer = 'xmlrpc' return True -The result of such a subscriber will be to replace any existing static -renderer configured by the developer with a (notional, nonexistent) XML-RPC -renderer if the request appears to come from an XML-RPC client. +The result of such a subscriber will be to replace any existing static renderer +configured by the developer with a (notional, nonexistent) XML-RPC renderer, if +the request appears to come from an XML-RPC client. diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index fa8ccc549..92139c0ff 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -3,50 +3,47 @@ Resources ========= -A :term:`resource` is an object that represents a "place" in a tree -related to your application. Every :app:`Pyramid` application has at -least one resource object: the :term:`root` resource. Even if you don't -define a root resource manually, a default one is created for you. The -root resource is the root of a :term:`resource tree`. A resource tree -is a set of nested dictionary-like objects which you can use to -represent your website's structure. +A :term:`resource` is an object that represents a "place" in a tree related to +your application. Every :app:`Pyramid` application has at least one resource +object: the :term:`root` resource. Even if you don't define a root resource +manually, a default one is created for you. The root resource is the root of a +:term:`resource tree`. A resource tree is a set of nested dictionary-like +objects which you can use to represent your website's structure. In an application which uses :term:`traversal` to map URLs to code, the resource tree structure is used heavily to map each URL to a :term:`view -callable`. When :term:`traversal` is used, :app:`Pyramid` will walk -through the resource tree by traversing through its nested dictionary -structure in order to find a :term:`context` resource. Once a context -resource is found, the context resource and data in the request will be -used to find a :term:`view callable`. +callable`. When :term:`traversal` is used, :app:`Pyramid` will walk through +the resource tree by traversing through its nested dictionary structure in +order to find a :term:`context` resource. Once a context resource is found, +the context resource and data in the request will be used to find a :term:`view +callable`. In an application which uses :term:`URL dispatch`, the resource tree is only used indirectly, and is often "invisible" to the developer. In URL dispatch applications, the resource "tree" is often composed of only the root resource -by itself. This root resource sometimes has security declarations attached -to it, but is not required to have any. In general, the resource tree is -much less important in applications that use URL dispatch than applications -that use traversal. +by itself. This root resource sometimes has security declarations attached to +it, but is not required to have any. In general, the resource tree is much +less important in applications that use URL dispatch than applications that use +traversal. In "Zope-like" :app:`Pyramid` applications, resource objects also often store data persistently, and offer methods related to mutating that persistent data. -In these kinds of applications, resources not only represent the site -structure of your website, but they become the :term:`domain model` of the -application. +In these kinds of applications, resources not only represent the site structure +of your website, but they become the :term:`domain model` of the application. Also: - The ``context`` and ``containment`` predicate arguments to :meth:`~pyramid.config.Configurator.add_view` (or a - :func:`~pyramid.view.view_config` decorator) reference a resource class - or resource :term:`interface`. + :func:`~pyramid.view.view_config` decorator) reference a resource class or + resource :term:`interface`. - A :term:`root factory` returns a resource. -- A resource is exposed to :term:`view` code as the :term:`context` of a - view. +- A resource is exposed to :term:`view` code as the :term:`context` of a view. -- Various helpful :app:`Pyramid` API methods expect a resource as an - argument (e.g. :func:`~pyramid.url.resource_url` and others). +- Various helpful :app:`Pyramid` API methods expect a resource as an argument + (e.g., :meth:`~pyramid.request.Request.resource_url` and others). .. index:: single: resource tree @@ -58,40 +55,39 @@ Also: Defining a Resource Tree ------------------------ -When :term:`traversal` is used (as opposed to a purely :term:`url dispatch` +When :term:`traversal` is used (as opposed to a purely :term:`URL dispatch` based application), :app:`Pyramid` expects to be able to traverse a tree -composed of resources (the :term:`resource tree`). Traversal begins at a -root resource, and descends into the tree recursively, trying each resource's +composed of resources (the :term:`resource tree`). Traversal begins at a root +resource, and descends into the tree recursively, trying each resource's ``__getitem__`` method to resolve a path segment to another resource object. -:app:`Pyramid` imposes the following policy on resource instances in the -tree: +:app:`Pyramid` imposes the following policy on resource instances in the tree: -- A container resource (a resource which contains other resources) must - supply a ``__getitem__`` method which is willing to resolve a unicode name - to a sub-resource. If a sub-resource by a particular name does not exist - in a container resource, ``__getitem__`` method of the container resource - must raise a :exc:`KeyError`. If a sub-resource by that name *does* exist, - the container's ``__getitem__`` should return the sub-resource. +- A container resource (a resource which contains other resources) must supply + a ``__getitem__`` method which is willing to resolve a Unicode name to a + sub-resource. If a sub-resource by a particular name does not exist in a + container resource, the ``__getitem__`` method of the container resource must + raise a :exc:`KeyError`. If a sub-resource by that name *does* exist, the + container's ``__getitem__`` should return the sub-resource. - Leaf resources, which do not contain other resources, must not implement a ``__getitem__``, or if they do, their ``__getitem__`` method must always raise a :exc:`KeyError`. -See :ref:`traversal_chapter` for more information about how traversal -works against resource instances. +See :ref:`traversal_chapter` for more information about how traversal works +against resource instances. Here's a sample resource tree, represented by a variable named ``root``: .. code-block:: python - :linenos: + :linenos: class Resource(dict): pass root = Resource({'a':Resource({'b':Resource({'c':Resource()})})}) -The resource tree we've created above is represented by a dictionary-like -root object which has a single child named ``'a'``. ``'a'`` has a single child +The resource tree we've created above is represented by a dictionary-like root +object which has a single child named ``'a'``. ``'a'`` has a single child named ``'b'``, and ``'b'`` has a single child named ``'c'``, which has no children. It is therefore possible to access the ``'c'`` leaf resource like so: @@ -100,20 +96,20 @@ children. It is therefore possible to access the ``'c'`` leaf resource like so: root['a']['b']['c'] -If you returned the above ``root`` object from a :term:`root factory`, the -path ``/a/b/c`` would find the ``'c'`` object in the resource tree as the -result of :term:`traversal`. +If you returned the above ``root`` object from a :term:`root factory`, the path +``/a/b/c`` would find the ``'c'`` object in the resource tree as the result of +:term:`traversal`. -In this example, each of the resources in the tree is of the same class. -This is not a requirement. Resource elements in the tree can be of any type. -We used a single class to represent all resources in the tree for the sake of +In this example, each of the resources in the tree is of the same class. This +is not a requirement. Resource elements in the tree can be of any type. We +used a single class to represent all resources in the tree for the sake of simplicity, but in a "real" app, the resources in the tree can be arbitrary. -Although the example tree above can service a traversal, the resource -instances in the above example are not aware of :term:`location`, so their -utility in a "real" application is limited. To make best use of built-in -:app:`Pyramid` API facilities, your resources should be "location-aware". -The next section details how to make resources location-aware. +Although the example tree above can service a traversal, the resource instances +in the above example are not aware of :term:`location`, so their utility in a +"real" application is limited. To make best use of built-in :app:`Pyramid` API +facilities, your resources should be "location-aware". The next section details +how to make resources location-aware. .. index:: pair: location-aware; resource @@ -125,16 +121,16 @@ Location-Aware Resources In order for certain :app:`Pyramid` location, security, URL-generation, and traversal APIs to work properly against the resources in a resource tree, all -resources in the tree must be :term:`location` -aware. This means they must +resources in the tree must be :term:`location`-aware. This means they must have two attributes: ``__parent__`` and ``__name__``. -The ``__parent__`` attribute of a location-aware resource should be a -reference to the resource's parent resource instance in the tree. The -``__name__`` attribute should be the name with which a resource's parent -refers to the resource via ``__getitem__``. +The ``__parent__`` attribute of a location-aware resource should be a reference +to the resource's parent resource instance in the tree. The ``__name__`` +attribute should be the name with which a resource's parent refers to the +resource via ``__getitem__``. -The ``__parent__`` of the root resource should be ``None`` and its -``__name__`` should be the empty string. For instance: +The ``__parent__`` of the root resource should be ``None`` and its ``__name__`` +should be the empty string. For instance: .. code-block:: python :linenos: @@ -143,62 +139,62 @@ The ``__parent__`` of the root resource should be ``None`` and its __name__ = '' __parent__ = None -A resource returned from the root resource's ``__getitem__`` method should -have a ``__parent__`` attribute that is a reference to the root resource, and -its ``__name__`` attribute should match the name by which it is reachable via -the root resource's ``__getitem__``. A container resource within the root -resource should have a ``__getitem__`` that returns resources with a -``__parent__`` attribute that points at the container, and these subobjects -should have a ``__name__`` attribute that matches the name by which they are -retrieved from the container via ``__getitem__``. This pattern continues -recursively "up" the tree from the root. +A resource returned from the root resource's ``__getitem__`` method should have +a ``__parent__`` attribute that is a reference to the root resource, and its +``__name__`` attribute should match the name by which it is reachable via the +root resource's ``__getitem__``. A container resource within the root resource +should have a ``__getitem__`` that returns resources with a ``__parent__`` +attribute that points at the container, and these sub-objects should have a +``__name__`` attribute that matches the name by which they are retrieved from +the container via ``__getitem__``. This pattern continues recursively "up" the +tree from the root. The ``__parent__`` attributes of each resource form a linked list that points -"downwards" toward the root. This is analogous to the `..` entry in +"downwards" toward the root. This is analogous to the ``..`` entry in filesystem directories. If you follow the ``__parent__`` values from any resource in the resource tree, you will eventually come to the root resource, just like if you keep executing the ``cd ..`` filesystem command, eventually you will reach the filesystem root directory. -.. warning:: If your root resource has a ``__name__`` argument - that is not ``None`` or the empty string, URLs returned by the - :func:`~pyramid.url.resource_url` function and paths generated by - the :func:`~pyramid.traversal.resource_path` and - :func:`~pyramid.traversal.resource_path_tuple` APIs will be - generated improperly. The value of ``__name__`` will be prepended - to every path and URL generated (as opposed to a single leading - slash or empty tuple element). +.. warning:: + + If your root resource has a ``__name__`` argument that is not ``None`` or + the empty string, URLs returned by the + :func:`~pyramid.request.Request.resource_url` function, and paths generated + by the :func:`~pyramid.traversal.resource_path` and + :func:`~pyramid.traversal.resource_path_tuple` APIs, will be generated + improperly. The value of ``__name__`` will be prepended to every path and + URL generated (as opposed to a single leading slash or empty tuple element). -.. sidebar:: Using :mod:`pyramid_traversalwrapper` +.. sidebar:: For your convenience - If you'd rather not manage the ``__name__`` and ``__parent__`` attributes - of your resources "by hand", an add-on package named + If you'd rather not manage the ``__name__`` and ``__parent__`` attributes of + your resources "by hand", an add-on package named :mod:`pyramid_traversalwrapper` can help. In order to use this helper feature, you must first install the :mod:`pyramid_traversalwrapper` package (available via PyPI), then register - its ``ModelGraphTraverser`` as the traversal policy, rather than the - default :app:`Pyramid` traverser. The package contains instructions for - doing so. - - Once :app:`Pyramid` is configured with this feature, you will no longer - need to manage the ``__parent__`` and ``__name__`` attributes on resource - objects "by hand". Instead, as necessary, during traversal :app:`Pyramid` - will wrap each resource (even the root resource) in a ``LocationProxy`` - which will dynamically assign a ``__name__`` and a ``__parent__`` to the - traversed resource (based on the last traversed resource and the name - supplied to ``__getitem__``). The root resource will have a ``__name__`` - attribute of ``None`` and a ``__parent__`` attribute of ``None``. - -Applications which use tree-walking :app:`Pyramid` APIs require -location-aware resources. These APIs include (but are not limited to) -:func:`~pyramid.url.resource_url`, :func:`~pyramid.traversal.find_resource`, -:func:`~pyramid.traversal.find_root`, + its ``ModelGraphTraverser`` as the traversal policy, rather than the default + :app:`Pyramid` traverser. The package contains instructions for doing so. + + Once :app:`Pyramid` is configured with this feature, you will no longer need + to manage the ``__parent__`` and ``__name__`` attributes on resource objects + "by hand". Instead, as necessary during traversal, :app:`Pyramid` will wrap + each resource (even the root resource) in a ``LocationProxy``, which will + dynamically assign a ``__name__`` and a ``__parent__`` to the traversed + resource, based on the last traversed resource and the name supplied to + ``__getitem__``. The root resource will have a ``__name__`` attribute of + ``None`` and a ``__parent__`` attribute of ``None``. + +Applications which use tree-walking :app:`Pyramid` APIs require location-aware +resources. These APIs include (but are not limited to) +:meth:`~pyramid.request.Request.resource_url`, +:func:`~pyramid.traversal.find_resource`, :func:`~pyramid.traversal.find_root`, :func:`~pyramid.traversal.find_interface`, :func:`~pyramid.traversal.resource_path`, -:func:`~pyramid.traversal.resource_path_tuple`, or +:func:`~pyramid.traversal.resource_path_tuple`, :func:`~pyramid.traversal.traverse`, :func:`~pyramid.traversal.virtual_root`, -and (usually) :func:`~pyramid.security.has_permission` and +and (usually) :meth:`~pyramid.request.Request.has_permission` and :func:`~pyramid.security.principals_allowed_by_permission`. In general, since so much :app:`Pyramid` infrastructure depends on @@ -211,104 +207,111 @@ location-aware. .. _generating_the_url_of_a_resource: -Generating The URL Of A Resource +Generating the URL of a Resource -------------------------------- -If your resources are :term:`location` aware, you can use the -:func:`pyramid.url.resource_url` API to generate a URL for the resource. -This URL will use the resource's position in the parent tree to create a -resource path, and it will prefix the path with the current application URL -to form a fully-qualified URL with the scheme, host, port, and path. You can -also pass extra arguments to :func:`~pyramid.url.resource_url` to influence -the generated URL. +If your resources are :term:`location`-aware, you can use the +:meth:`pyramid.request.Request.resource_url` API to generate a URL for the +resource. This URL will use the resource's position in the parent tree to +create a resource path, and it will prefix the path with the current +application URL to form a fully-qualified URL with the scheme, host, port, and +path. You can also pass extra arguments to +:meth:`~pyramid.request.Request.resource_url` to influence the generated URL. -The simplest call to :func:`~pyramid.url.resource_url` looks like this: +The simplest call to :meth:`~pyramid.request.Request.resource_url` looks like +this: .. code-block:: python :linenos: - from pyramid.url import resource_url - url = resource_url(resource, request) + url = request.resource_url(resource) -The ``request`` passed to ``resource_url`` in the above example is an -instance of a :app:`Pyramid` :term:`request` object. +The ``request`` in the above example is an instance of a :app:`Pyramid` +:term:`request` object. If the resource referred to as ``resource`` in the above example was the root -resource, and the host that was used to contact the server was -``example.com``, the URL generated would be ``http://example.com/``. -However, if the resource was a child of the root resource named ``a``, the -generated URL would be ``http://example.com/a/``. +resource, and the host that was used to contact the server was ``example.com``, +the URL generated would be ``http://example.com/``. However, if the resource +was a child of the root resource named ``a``, the generated URL would be +``http://example.com/a/``. A slash is appended to all resource URLs when -:func:`~pyramid.url.resource_url` is used to generate them in this simple -manner, because resources are "places" in the hierarchy, and URLs are meant -to be clicked on to be visited. Relative URLs that you include on HTML pages -rendered as the result of the default view of a resource are more -apt to be relative to these resources than relative to their parent. +:meth:`~pyramid.request.Request.resource_url` is used to generate them in this +simple manner, because resources are "places" in the hierarchy, and URLs are +meant to be clicked on to be visited. Relative URLs that you include on HTML +pages rendered as the result of the default view of a resource are more apt to +be relative to these resources than relative to their parent. -You can also pass extra elements to :func:`~pyramid.url.resource_url`: +You can also pass extra elements to +:meth:`~pyramid.request.Request.resource_url`: .. code-block:: python :linenos: - from pyramid.url import resource_url - url = resource_url(resource, request, 'foo', 'bar') + url = request.resource_url(resource, 'foo', 'bar') If the resource referred to as ``resource`` in the above example was the root -resource, and the host that was used to contact the server was -``example.com``, the URL generated would be ``http://example.com/foo/bar``. -Any number of extra elements can be passed to -:func:`~pyramid.url.resource_url` as extra positional arguments. When extra -elements are passed, they are appended to the resource's URL. A slash is not -appended to the final segment when elements are passed. +resource, and the host that was used to contact the server was ``example.com``, +the URL generated would be ``http://example.com/foo/bar``. Any number of extra +elements can be passed to :meth:`~pyramid.request.Request.resource_url` as +extra positional arguments. When extra elements are passed, they are appended +to the resource's URL. A slash is not appended to the final segment when +elements are passed. You can also pass a query string: .. code-block:: python :linenos: - from pyramid.url import resource_url - url = resource_url(resource, request, query={'a':'1'}) + url = request.resource_url(resource, query={'a':'1'}) If the resource referred to as ``resource`` in the above example was the root -resource, and the host that was used to contact the server was -``example.com``, the URL generated would be ``http://example.com/?a=1``. +resource, and the host that was used to contact the server was ``example.com``, +the URL generated would be ``http://example.com/?a=1``. When a :term:`virtual root` is active, the URL generated by -:func:`~pyramid.url.resource_url` for a resource may be "shorter" than its -physical tree path. See :ref:`virtual_root_support` for more information -about virtually rooting a resource. +:meth:`~pyramid.request.Request.resource_url` for a resource may be "shorter" +than its physical tree path. See :ref:`virtual_root_support` for more +information about virtually rooting a resource. -The shortcut method of the :term:`request` named -:meth:`pyramid.request.Request.resource_url` can be used instead of -:func:`~pyramid.url.resource_url` to generate a resource URL. +For more information about generating resource URLs, see the documentation for +:meth:`pyramid.request.Request.resource_url`. -For more information about generating resource URLs, see the documentation -for :func:`pyramid.url.resource_url`. +.. index:: + pair: resource URL generation; overriding .. _overriding_resource_url_generation: Overriding Resource URL Generation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If a resource object implements a ``__resource_url__`` method, this method -will be called when :func:`~pyramid.url.resource_url` is called to generate a -URL for the resource, overriding the default URL returned for the resource by -:func:`~pyramid.url.resource_url`. +If a resource object implements a ``__resource_url__`` method, this method will +be called when :meth:`~pyramid.request.Request.resource_url` is called to +generate a URL for the resource, overriding the default URL returned for the +resource by :meth:`~pyramid.request.Request.resource_url`. The ``__resource_url__`` hook is passed two arguments: ``request`` and ``info``. ``request`` is the :term:`request` object passed to -:func:`~pyramid.url.resource_url`. ``info`` is a dictionary with two -keys: +:meth:`~pyramid.request.Request.resource_url`. ``info`` is a dictionary with +the following keys: ``physical_path`` - The "physical path" computed for the resource, as defined by - ``pyramid.traversal.resource_path(resource)``. + A string representing the "physical path" computed for the resource, as + defined by ``pyramid.traversal.resource_path(resource)``. It will begin and + end with a slash. ``virtual_path`` - The "virtual path" computed for the resource, as defined by - :ref:`virtual_root_support`. This will be identical to the physical path - if virtual rooting is not enabled. + A string representing the "virtual path" computed for the resource, as + defined by :ref:`virtual_root_support`. This will be identical to the + physical path if virtual rooting is not enabled. It will begin and end with + a slash. + +``app_url`` + A string representing the application URL generated during + ``request.resource_url``. It will not end with a slash. It represents a + potentially customized URL prefix, containing potentially custom scheme, host + and port information passed by the user to ``request.resource_url``. It + should be preferred over use of ``request.application_url``. The ``__resource_url__`` method of a resource should return a string representing a URL. If it cannot override the default, it should return @@ -321,25 +324,27 @@ Here's an example ``__resource_url__`` method. class Resource(object): def __resource_url__(self, request, info): - return request.application_url + info['virtual_path'] + return info['app_url'] + info['virtual_path'] The above example actually just generates and returns the default URL, which -would have been what was returned anyway, but your code can perform arbitrary -logic as necessary. For example, your code may wish to override the hostname -or port number of the generated URL. +would have been what was generated by the default ``resource_url`` machinery, +but your code can perform arbitrary logic as necessary. For example, your code +may wish to override the hostname or port number of the generated URL. + +Note that the URL generated by ``__resource_url__`` should be fully qualified, +should end in a slash, and should not contain any query string or anchor +elements (only path elements) to work with +:meth:`~pyramid.request.Request.resource_url`. -Note that the URL generated by ``__resource_url__`` should be fully -qualified, should end in a slash, and should not contain any query string or -anchor elements (only path elements) to work best with -:func:`~pyramid.url.resource_url`. +.. index:: + single: resource path generation Generating the Path To a Resource --------------------------------- :func:`pyramid.traversal.resource_path` returns a string object representing -the absolute physical path of the resource object based on its position in -the resource tree. Each segment of the path is separated with a slash -character. +the absolute physical path of the resource object based on its position in the +resource tree. Each segment of the path is separated with a slash character. .. code-block:: python :linenos: @@ -365,14 +370,17 @@ If ``resource`` in the example above was accessible in the tree as The resource passed in must be :term:`location`-aware. -The presence or absence of a :term:`virtual root` has no impact on the -behavior of :func:`~pyramid.traversal.resource_path`. +The presence or absence of a :term:`virtual root` has no impact on the behavior +of :func:`~pyramid.traversal.resource_path`. + +.. index:: + pair: resource; finding by path Finding a Resource by Path -------------------------- -If you have a string path to a resource, you can grab the resource from -that place in the application's resource tree using +If you have a string path to a resource, you can grab the resource from that +place in the application's resource tree using :func:`pyramid.traversal.find_resource`. You can resolve an absolute path by passing a string prefixed with a ``/`` as @@ -384,8 +392,9 @@ the ``path`` argument: from pyramid.traversal import find_resource url = find_resource(anyresource, '/path') -Or you can resolve a path relative to the resource you pass in by passing a -string that isn't prefixed by ``/``: +Or you can resolve a path relative to the resource that you pass in to +:func:`pyramid.traversal.find_resource` by passing a string that isn't prefixed +by ``/``: .. code-block:: python :linenos: @@ -394,8 +403,8 @@ string that isn't prefixed by ``/``: url = find_resource(anyresource, 'path') Often the paths you pass to :func:`~pyramid.traversal.find_resource` are -generated by the :func:`~pyramid.traversal.resource_path` API. These APIs -are "mirrors" of each other. +generated by the :func:`~pyramid.traversal.resource_path` API. These APIs are +"mirrors" of each other. If the path cannot be resolved when calling :func:`~pyramid.traversal.find_resource` (if the respective resource in the @@ -404,14 +413,17 @@ tree does not exist), a :exc:`KeyError` will be raised. See the :func:`pyramid.traversal.find_resource` documentation for more information about resolving a path to a resource. +.. index:: + pair: resource; lineage + Obtaining the Lineage of a Resource ----------------------------------- :func:`pyramid.location.lineage` returns a generator representing the -:term:`lineage` of the :term:`location` aware :term:`resource` object. +:term:`lineage` of the :term:`location`-aware :term:`resource` object. -The :func:`~pyramid.location.lineage` function returns the resource it is -passed, then each parent of the resource, in order. For example, if the +The :func:`~pyramid.location.lineage` function returns the resource that is +passed into it, then each parent of the resource in order. For example, if the resource tree is composed like so: .. code-block:: python @@ -432,18 +444,18 @@ list, we will get: list(lineage(thing2)) [ <Thing object at thing2>, <Thing object at thing1> ] -The generator returned by :func:`~pyramid.location.lineage` first returns the -resource it was passed unconditionally. Then, if the resource supplied a -``__parent__`` attribute, it returns the resource represented by -``resource.__parent__``. If *that* resource has a ``__parent__`` attribute, -return that resource's parent, and so on, until the resource being inspected -either has no ``__parent__`` attribute or has a ``__parent__`` attribute of -``None``. +The generator returned by :func:`~pyramid.location.lineage` first returns +unconditionally the resource that was passed into it. Then, if the resource +supplied a ``__parent__`` attribute, it returns the resource represented by +``resource.__parent__``. If *that* resource has a ``__parent__`` attribute, it +will return that resource's parent, and so on, until the resource being +inspected either has no ``__parent__`` attribute or has a ``__parent__`` +attribute of ``None``. See the documentation for :func:`pyramid.location.lineage` for more information. -Determining if a Resource is In The Lineage of Another Resource +Determining if a Resource is in the Lineage of Another Resource --------------------------------------------------------------- Use the :func:`pyramid.location.inside` function to determine if one resource @@ -460,24 +472,27 @@ For example, if the resource tree is: b = Thing() b.__parent__ = a -Calling ``inside(b, a)`` will return ``True``, because ``b`` has a lineage -that includes ``a``. However, calling ``inside(a, b)`` will return ``False`` +Calling ``inside(b, a)`` will return ``True``, because ``b`` has a lineage that +includes ``a``. However, calling ``inside(a, b)`` will return ``False`` because ``a`` does not have a lineage that includes ``b``. The argument list for :func:`~pyramid.location.inside` is ``(resource1, -resource2)``. ``resource1`` is 'inside' ``resource2`` if ``resource2`` is a +resource2)``. ``resource1`` is "inside" ``resource2`` if ``resource2`` is a :term:`lineage` ancestor of ``resource1``. It is a lineage ancestor if its parent (or one of its parent's parents, etc.) is an ancestor. See :func:`pyramid.location.inside` for more information. +.. index:: + pair: resource; finding root + Finding the Root Resource ------------------------- Use the :func:`pyramid.traversal.find_root` API to find the :term:`root` -resource. The root resource is the root resource of the :term:`resource -tree`. The API accepts a single argument: ``resource``. This is a resource -that is :term:`location` aware. It can be any resource in the tree for which +resource. The root resource is the resource at the root of the :term:`resource +tree`. The API accepts a single argument: ``resource``. This is a resource +that is :term:`location`-aware. It can be any resource in the tree for which you want to find the root. For example, if the resource tree is: @@ -496,9 +511,9 @@ Calling ``find_root(b)`` will return ``a``. The root resource is also available as ``request.root`` within :term:`view callable` code. -The presence or absence of a :term:`virtual root` has no impact on the -behavior of :func:`~pyramid.traversal.find_root`. The root object returned -is always the *physical* root object. +The presence or absence of a :term:`virtual root` has no impact on the behavior +of :func:`~pyramid.traversal.find_root`. The root object returned is always +the *physical* root object. .. index:: single: resource interfaces @@ -509,32 +524,31 @@ Resources Which Implement Interfaces ------------------------------------ Resources can optionally be made to implement an :term:`interface`. An -interface is used to tag a resource object with a "type" that can later be +interface is used to tag a resource object with a "type" that later can be referred to within :term:`view configuration` and by :func:`pyramid.traversal.find_interface`. Specifying an interface instead of a class as the ``context`` or ``containment`` predicate arguments within :term:`view configuration` statements makes it possible to use a single view callable for more than one -class of resource object. If your application is simple enough that you see -no reason to want to do this, you can skip reading this section of the -chapter. +class of resource objects. If your application is simple enough that you see +no reason to want to do this, you can skip reading this section of the chapter. -For example, here's some code which describes a blog entry which also -declares that the blog entry implements an :term:`interface`. +For example, here's some code which describes a blog entry which also declares +that the blog entry implements an :term:`interface`. .. code-block:: python :linenos: import datetime - from zope.interface import implements + from zope.interface import implementer from zope.interface import Interface class IBlogEntry(Interface): pass + @implementer(IBlogEntry) class BlogEntry(object): - implements(IBlogEntry) def __init__(self, title, body, author): self.title = title self.body = body @@ -543,22 +557,22 @@ declares that the blog entry implements an :term:`interface`. This resource consists of two things: the class which defines the resource constructor as the class ``BlogEntry``, and an :term:`interface` attached to -the class via an ``implements`` statement at class scope using the -``IBlogEntry`` interface as its sole argument. +the class via an ``implementer`` class decorator using the ``IBlogEntry`` +interface as its sole argument. The interface object used must be an instance of a class that inherits from :class:`zope.interface.Interface`. A resource class may implement zero or more interfaces. You specify that a resource implements an interface by using the -:func:`zope.interface.implements` function at class scope. The above +:func:`zope.interface.implementer` function as a class decorator. The above ``BlogEntry`` resource implements the ``IBlogEntry`` interface. You can also specify that a particular resource *instance* provides an -interface, as opposed to its class. When you declare that a class implements -an interface, all instances of that class will also provide that interface. -However, you can also just say that a single object provides the interface. -To do so, use the :func:`zope.interface.directlyProvides` function: +interface as opposed to its class. When you declare that a class implements an +interface, all instances of that class will also provide that interface. +However, you can also just say that a single object provides the interface. To +do so, use the :func:`zope.interface.directlyProvides` function: .. code-block:: python :linenos: @@ -581,9 +595,9 @@ To do so, use the :func:`zope.interface.directlyProvides` function: directlyProvides(entry, IBlogEntry) :func:`zope.interface.directlyProvides` will replace any existing interface -that was previously provided by an instance. If a resource object already -has instance-level interface declarations that you don't want to replace, use -the :func:`zope.interface.alsoProvides` function: +that was previously provided by an instance. If a resource object already has +instance-level interface declarations that you don't want to replace, use the +:func:`zope.interface.alsoProvides` function: .. code-block:: python :linenos: @@ -610,14 +624,17 @@ the :func:`zope.interface.alsoProvides` function: directlyProvides(entry, IBlogEntry1) alsoProvides(entry, IBlogEntry2) -:func:`zope.interface.alsoProvides` will augment the set of interfaces -directly provided by an instance instead of overwriting them like +:func:`zope.interface.alsoProvides` will augment the set of interfaces directly +provided by an instance instead of overwriting them like :func:`zope.interface.directlyProvides` does. For more information about how resource interfaces can be used by view configuration, see :ref:`using_resource_interfaces`. -Finding a Resource With a Class or Interface in Lineage +.. index:: + pair: resource; finding by interface or class + +Finding a Resource with a Class or Interface in Lineage ------------------------------------------------------- Use the :func:`~pyramid.traversal.find_interface` API to locate a parent that @@ -637,19 +654,23 @@ For example, if your resource tree is composed as follows: Calling ``find_interface(a, Thing1)`` will return the ``a`` resource because ``a`` is of class ``Thing1`` (the resource passed as the first argument is -considered first, and is returned if the class or interface spec matches). +considered first, and is returned if the class or interface specification +matches). Calling ``find_interface(b, Thing1)`` will return the ``a`` resource because -``a`` is of class ``Thing1`` and ``a`` is the first resource in ``b``'s -lineage of this class. +``a`` is of class ``Thing1`` and ``a`` is the first resource in ``b``'s lineage +of this class. Calling ``find_interface(b, Thing2)`` will return the ``b`` resource. -The second argument to find_interface may also be a :term:`interface` instead -of a class. If it is an interface, each resource in the lineage is checked -to see if the resource implements the specificed interface (instead of seeing -if the resource is of a class). See also -:ref:`resources_which_implement_interfaces`. +The second argument to ``find_interface`` may also be a :term:`interface` +instead of a class. If it is an interface, each resource in the lineage is +checked to see if the resource implements the specificed interface (instead of +seeing if the resource is of a class). + +.. seealso:: + + See also :ref:`resources_which_implement_interfaces`. .. index:: single: resource API functions @@ -662,18 +683,17 @@ A resource object is used as the :term:`context` provided to a view. See :ref:`traversal_chapter` and :ref:`urldispatch_chapter` for more information about how a resource object becomes the context. -The APIs provided by :ref:`traversal_module` are used against resource -objects. These functions can be used to find the "path" of a resource, the -root resource in a resource tree, or to generate a URL for a resource. - -The APIs provided by :ref:`location_module` are used against resources. -These can be used to walk down a resource tree, or conveniently locate one -resource "inside" another. +The APIs provided by :ref:`traversal_module` are used against resource objects. +These functions can be used to find the "path" of a resource, the root resource +in a resource tree, or to generate a URL for a resource. -Some APIs in :ref:`security_module` accept a resource object as a parameter. -For example, the :func:`~pyramid.security.has_permission` API accepts a -resource object as one of its arguments; the ACL is obtained from this -resource or one of its ancestors. Other APIs in the :mod:`pyramid.security` -module also accept :term:`context` as an argument, and a context is always a -resource. +The APIs provided by :ref:`location_module` are used against resources. These +can be used to walk down a resource tree, or conveniently locate one resource +"inside" another. +Some APIs on the :class:`pyramid.request.Request` accept a resource object as a +parameter. For example, the :meth:`~pyramid.request.Request.has_permission` API +accepts a resource object as one of its arguments; the ACL is obtained from +this resource or one of its ancestors. Other security related APIs on the +:class:`pyramid.request.Request` class also accept :term:`context` as an +argument, and a context is always a resource. diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 11f84d4ea..e45e6f4a8 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -2,137 +2,130 @@ single: request processing single: request single: router + single: request lifecycle .. _router_chapter: Request Processing ================== -Once a :app:`Pyramid` application is up and running, it is ready to -accept requests and return responses. +.. image:: ../_static/pyramid_request_processing.* + :alt: Request Processing -What happens from the time a :term:`WSGI` request enters a -:app:`Pyramid` application through to the point that -:app:`Pyramid` hands off a response back to WSGI for upstream -processing? +Once a :app:`Pyramid` application is up and running, it is ready to accept +requests and return responses. What happens from the time a :term:`WSGI` +request enters a :app:`Pyramid` application through to the point that +:app:`Pyramid` hands off a response back to WSGI for upstream processing? -#. A user initiates a request from his browser to the hostname and - port number of the WSGI server used by the :app:`Pyramid` - application. +#. A user initiates a request from their browser to the hostname and port + number of the WSGI server used by the :app:`Pyramid` application. -#. The WSGI server used by the :app:`Pyramid` application passes - the WSGI environment to the ``__call__`` method of the - :app:`Pyramid` :term:`router` object. +#. The WSGI server used by the :app:`Pyramid` application passes the WSGI + environment to the ``__call__`` method of the :app:`Pyramid` :term:`router` + object. #. A :term:`request` object is created based on the WSGI environment. -#. The :term:`application registry` and the :term:`request` object - created in the last step are pushed on to the :term:`thread local` - stack that :app:`Pyramid` uses to allow the functions named +#. The :term:`application registry` and the :term:`request` object created in + the last step are pushed on to the :term:`thread local` stack that + :app:`Pyramid` uses to allow the functions named :func:`~pyramid.threadlocal.get_current_request` and :func:`~pyramid.threadlocal.get_current_registry` to work. #. A :class:`~pyramid.events.NewRequest` :term:`event` is sent to any subscribers. -#. If any :term:`route` has been defined within application - configuration, the :app:`Pyramid` :term:`router` calls a - :term:`URL dispatch` "route mapper." The job of the mapper is to - examine the request to determine whether any user-defined - :term:`route` matches the current WSGI environment. The +#. If any :term:`route` has been defined within application configuration, the + :app:`Pyramid` :term:`router` calls a :term:`URL dispatch` "route mapper." + The job of the mapper is to examine the request to determine whether any + user-defined :term:`route` matches the current WSGI environment. The :term:`router` passes the request as an argument to the mapper. -#. If any route matches, the request is mutated; a ``matchdict`` and - ``matched_route`` attributes are added to the request object; the - former contains a dictionary representing the matched dynamic - elements of the request's ``PATH_INFO`` value, the latter contains - the :class:`~pyramid.interfaces.IRoute` object representing the - route which matched. The root object associated with the route - found is also generated: if the :term:`route configuration` which - matched has an associated a ``factory`` argument, this factory is - used to generate the root object, otherwise a default :term:`root - factory` is used. - -#. If a route match was *not* found, and a ``root_factory`` argument - was passed to the :term:`Configurator` constructor, that callable - is used to generate the root object. If the ``root_factory`` - argument passed to the Configurator constructor was ``None``, a - default root factory is used to generate a root object. - -#. The :app:`Pyramid` router calls a "traverser" function with the - root object and the request. The traverser function attempts to - traverse the root object (using any existing ``__getitem__`` on the - root object and subobjects) to find a :term:`context`. If the root - object has no ``__getitem__`` method, the root itself is assumed to - be the context. The exact traversal algorithm is described in - :ref:`traversal_chapter`. The traverser function returns a - dictionary, which contains a :term:`context` and a :term:`view - name` as well as other ancillary information. - -#. The request is decorated with various names returned from the - traverser (such as ``context``, ``view_name``, and so forth), so - they can be accessed via e.g. ``request.context`` within - :term:`view` code. - -#. A :class:`~pyramid.events.ContextFound` :term:`event` is - sent to any subscribers. - -#. :app:`Pyramid` looks up a :term:`view` callable using the - context, the request, and the view name. If a view callable - doesn't exist for this combination of objects (based on the type of - the context, the type of the request, and the value of the view - name, and any :term:`predicate` attributes applied to the view - configuration), :app:`Pyramid` raises a - :class:`~pyramid.exceptions.NotFound` exception, which is meant - to be caught by a surrounding exception handler. - -#. If a view callable was found, :app:`Pyramid` attempts to call - the view function. - -#. If an :term:`authorization policy` is in use, and the view was - protected by a :term:`permission`, :app:`Pyramid` passes the - context, the request, and the view_name to a function which - determines whether the view being asked for can be executed by the - requesting user, based on credential information in the request and - security information attached to the context. If it returns - ``True``, :app:`Pyramid` calls the view callable to obtain a - response. If it returns ``False``, it raises a - :class:`~pyramid.exceptions.Forbidden` exception, which is meant - to be called by a surrounding exception handler. - -#. If any exception was raised within a :term:`root factory`, by - :term:`traversal`, by a :term:`view callable` or by - :app:`Pyramid` itself (such as when it raises - :class:`~pyramid.exceptions.NotFound` or - :class:`~pyramid.exceptions.Forbidden`), the router catches the - exception, and attaches it to the request as the ``exception`` - attribute. It then attempts to find a :term:`exception view` for - the exception that was caught. If it finds an exception view - callable, that callable is called, and is presumed to generate a - response. If an :term:`exception view` that matches the exception - cannot be found, the exception is reraised. - -#. The following steps occur only when a :term:`response` could be - successfully generated by a normal :term:`view callable` or an - :term:`exception view` callable. :app:`Pyramid` will attempt to execute - any :term:`response callback` functions attached via - :meth:`~pyramid.request.Request.add_response_callback`. A - :class:`~pyramid.events.NewResponse` :term:`event` is then sent to any - subscribers. The response object's ``app_iter``, ``status``, and - ``headerlist`` attributes are then used to generate a WSGI response. The - response is sent back to the upstream WSGI server. +#. If any route matches, the route mapper adds the attributes ``matchdict`` + and ``matched_route`` to the request object. The former contains a + dictionary representing the matched dynamic elements of the request's + ``PATH_INFO`` value, and the latter contains the + :class:`~pyramid.interfaces.IRoute` object representing the route which + matched. + +#. A :class:`~pyramid.events.BeforeTraversal` :term:`event` is sent to any + subscribers. + +#. Continuing, if any route matches, the root object associated with the found + route is generated. If the :term:`route configuration` which matched has an + associated ``factory`` argument, then this factory is used to generate the + root object; otherwise a default :term:`root factory` is used. + + However, if no route matches, and if a ``root_factory`` argument was passed + to the :term:`Configurator` constructor, that callable is used to generate + the root object. If the ``root_factory`` argument passed to the + Configurator constructor was ``None``, a default root factory is used to + generate a root object. + +#. The :app:`Pyramid` router calls a "traverser" function with the root object + and the request. The traverser function attempts to traverse the root + object (using any existing ``__getitem__`` on the root object and + subobjects) to find a :term:`context`. If the root object has no + ``__getitem__`` method, the root itself is assumed to be the context. The + exact traversal algorithm is described in :ref:`traversal_chapter`. The + traverser function returns a dictionary, which contains a :term:`context` + and a :term:`view name` as well as other ancillary information. + +#. The request is decorated with various names returned from the traverser + (such as ``context``, ``view_name``, and so forth), so they can be accessed + via, for example, ``request.context`` within :term:`view` code. + +#. A :class:`~pyramid.events.ContextFound` :term:`event` is sent to any + subscribers. -#. :app:`Pyramid` will attempt to execute any :term:`finished +#. :app:`Pyramid` looks up a :term:`view` callable using the context, the + request, and the view name. If a view callable doesn't exist for this + combination of objects (based on the type of the context, the type of the + request, and the value of the view name, and any :term:`predicate` + attributes applied to the view configuration), :app:`Pyramid` raises a + :class:`~pyramid.httpexceptions.HTTPNotFound` exception, which is meant to + be caught by a surrounding :term:`exception view`. + +#. If a view callable was found, :app:`Pyramid` attempts to call it. If an + :term:`authorization policy` is in use, and the view configuration is + protected by a :term:`permission`, :app:`Pyramid` determines whether the + view callable being asked for can be executed by the requesting user based + on credential information in the request and security information attached + to the context. If the view execution is allowed, :app:`Pyramid` calls the + view callable to obtain a response. If view execution is forbidden, + :app:`Pyramid` raises a :class:`~pyramid.httpexceptions.HTTPForbidden` + exception. + +#. If any exception is raised within a :term:`root factory`, by + :term:`traversal`, by a :term:`view callable`, or by :app:`Pyramid` itself + (such as when it raises :class:`~pyramid.httpexceptions.HTTPNotFound` or + :class:`~pyramid.httpexceptions.HTTPForbidden`), the router catches the + exception, and attaches it to the request as the ``exception`` attribute. + It then attempts to find a :term:`exception view` for the exception that was + caught. If it finds an exception view callable, that callable is called, + and is presumed to generate a response. If an :term:`exception view` that + matches the exception cannot be found, the exception is reraised. + +#. The following steps occur only when a :term:`response` could be successfully + generated by a normal :term:`view callable` or an :term:`exception view` + callable. :app:`Pyramid` will attempt to execute any :term:`response callback` functions attached via + :meth:`~pyramid.request.Request.add_response_callback`. A + :class:`~pyramid.events.NewResponse` :term:`event` is then sent to any + subscribers. The response object's ``__call__`` method is then used to + generate a WSGI response. The response is sent back to the upstream WSGI + server. + +#. :app:`Pyramid` will attempt to execute any :term:`finished callback` + functions attached via :meth:`~pyramid.request.Request.add_finished_callback`. #. The :term:`thread local` stack is popped. -.. image:: router.png - -This is a very high-level overview that leaves out various details. -For more detail about subsystems invoked by the :app:`Pyramid` router -such as traversal, URL dispatch, views, and event processing, see -:ref:`urldispatch_chapter`, :ref:`views_chapter`, and -:ref:`events_chapter`. +.. image:: ../_static/pyramid_router.* + :alt: Pyramid Router +This is a very high-level overview that leaves out various details. For more +detail about subsystems invoked by the :app:`Pyramid` router, such as +traversal, URL dispatch, views, and event processing, see +:ref:`urldispatch_chapter`, :ref:`views_chapter`, and :ref:`events_chapter`. diff --git a/docs/narr/scaffolding.rst b/docs/narr/scaffolding.rst new file mode 100644 index 000000000..164ceb3bf --- /dev/null +++ b/docs/narr/scaffolding.rst @@ -0,0 +1,180 @@ +.. _scaffolding_chapter: + +Creating Pyramid Scaffolds +========================== + +You can extend Pyramid by creating a :term:`scaffold` template. A scaffold +template is useful if you'd like to distribute a customizable configuration of +Pyramid to other users. Once you've created a scaffold, and someone has +installed the distribution that houses the scaffold, they can use the +``pcreate`` script to create a custom version of your scaffold's template. +Pyramid itself uses scaffolds to allow people to bootstrap new projects. For +example, ``pcreate -s alchemy MyStuff`` causes Pyramid to render the +``alchemy`` scaffold template to the ``MyStuff`` directory. + +Basics +------ + +A scaffold template is just a bunch of source files and directories on disk. A +small definition class points at this directory. It is in turn pointed at by a +:term:`setuptools` "entry point" which registers the scaffold so it can be +found by the ``pcreate`` command. + +To create a scaffold template, create a Python :term:`distribution` to house +the scaffold which includes a ``setup.py`` that relies on the ``setuptools`` +package. See `Packaging and Distributing Projects +<https://packaging.python.org/en/latest/distributing/>`_ for more information +about how to do this. For example, we'll pretend the distribution you create +is named ``CoolExtension``, and it has a package directory within it named +``coolextension``. + +Once you've created the distribution, put a "scaffolds" directory within your +distribution's package directory, and create a file within that directory named +``__init__.py`` with something like the following: + +.. code-block:: python + :linenos: + + # CoolExtension/coolextension/scaffolds/__init__.py + + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool extension' + +Once this is done, within the ``scaffolds`` directory, create a template +directory. Our example used a template directory named +``coolextension_scaffold``. + +As you create files and directories within the template directory, note that: + +- Files which have a name which are suffixed with the value ``_tmpl`` will be + rendered, and replacing any instance of the literal string ``{{var}}`` with + the string value of the variable named ``var`` provided to the scaffold. + +- Files and directories with filenames that contain the string ``+var+`` will + have that string replaced with the value of the ``var`` variable provided to + the scaffold. + +- Files that start with a dot (e.g., ``.env``) are ignored and will not be + copied over to the destination directory. If you want to include a file with + a leading dot, then you must replace the dot with ``+dot+`` (e.g., + ``+dot+env``). + +Otherwise, files and directories which live in the template directory will be +copied directly without modification to the ``pcreate`` output location. + +The variables provided by the default ``PyramidTemplate`` include ``project`` +(the project name provided by the user as an argument to ``pcreate``), +``package`` (a lowercasing and normalizing of the project name provided by the +user), ``random_string`` (a long random string), and ``package_logger`` (the +name of the package's logger). + +See Pyramid's "scaffolds" package +(https://github.com/Pylons/pyramid/tree/master/pyramid/scaffolds) for concrete +examples of scaffold directories (``zodb``, ``alchemy``, and ``starter``, for +example). + +After you've created the template directory, add the following to the +``entry_points`` value of your distribution's ``setup.py``: + +.. code-block:: ini + + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + +For example: + +.. code-block:: python + + def setup( + ..., + entry_points = """\ + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + """ + ) + +Run your distribution's ``setup.py develop`` or ``setup.py install`` command. +After that, you should be able to see your scaffolding template listed when you +run ``pcreate -l``. It will be named ``coolextension`` because that's the name +we gave it in the entry point setup. Running ``pcreate -s coolextension +MyStuff`` will then render your scaffold to an output directory named +``MyStuff``. + +See the module documentation for :mod:`pyramid.scaffolds` for information about +the API of the :class:`pyramid.scaffolds.Template` class and related classes. +You can override methods of this class to get special behavior. + +Supporting Older Pyramid Versions +--------------------------------- + +Because different versions of Pyramid handled scaffolding differently, if you +want to have extension scaffolds that can work across Pyramid 1.0.X, 1.1.X, +1.2.X and 1.3.X, you'll need to use something like this bit of horror while +defining your scaffold template: + +.. code-block:: python + :linenos: + + try: # pyramid 1.0.X + # "pyramid.paster.paste_script..." doesn't exist past 1.0.X + from pyramid.paster import paste_script_template_renderer + from pyramid.paster import PyramidTemplate + except ImportError: + try: # pyramid 1.1.X, 1.2.X + # trying to import "paste_script_template_renderer" fails on 1.3.X + from pyramid.scaffolds import paste_script_template_renderer + from pyramid.scaffolds import PyramidTemplate + except ImportError: # pyramid >=1.3a2 + paste_script_template_renderer = None + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool extension' + template_renderer = staticmethod(paste_script_template_renderer) + +And then in the setup.py of the package that contains your scaffold, define +the template as a target of both ``paste.paster_create_template`` (for +``paster create``) and ``pyramid.scaffold`` (for ``pcreate``). + +.. code-block:: ini + + [paste.paster_create_template] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + +Doing this hideousness will allow your scaffold to work as a ``paster create`` +target (under 1.0, 1.1, or 1.2) or as a ``pcreate`` target (under 1.3). If an +invoker tries to run ``paster create`` against a scaffold defined this way +under 1.3, an error is raised instructing them to use ``pcreate`` instead. + +If you want to support Pyramid 1.3 only, it's much cleaner, and the API is +stable: + +.. code-block:: python + :linenos: + + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool_extension' + +You only need to specify a ``paste.paster_create_template`` entry point target +in your ``setup.py`` if you want your scaffold to be consumable by users of +Pyramid 1.0, 1.1, or 1.2. To support only 1.3, specifying only the +``pyramid.scaffold`` entry point is good enough. If you want to support both +``paster create`` and ``pcreate`` (meaning you want to support Pyramid 1.2 and +some older version), you'll need to define both. + +Examples +-------- + +Existing third-party distributions which house scaffolding are available via +:term:`PyPI`. The ``pyramid_jqm``, ``pyramid_zcml``, and ``pyramid_jinja2`` +packages house scaffolds. You can install and examine these packages to see +how they work in the quest to develop your own scaffolding. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index c7a07b857..7cbea113c 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -6,115 +6,120 @@ Security ======== -:app:`Pyramid` provides an optional declarative authorization system -that can prevent a :term:`view` from being invoked based on an -:term:`authorization policy`. Before a view is invoked, the -authorization system can use the credentials in the :term:`request` -along with the :term:`context` resource to determine if access will be -allowed. Here's how it works at a high level: +:app:`Pyramid` provides an optional, declarative, security system. Security in +:app:`Pyramid` is separated into authentication and authorization. The two +systems communicate via :term:`principal` identifiers. Authentication is merely +the mechanism by which credentials provided in the :term:`request` are resolved +to one or more :term:`principal` identifiers. These identifiers represent the +users and groups that are in effect during the request. Authorization then +determines access based on the :term:`principal` identifiers, the requested +:term:`permission`, and a :term:`context`. + +The :app:`Pyramid` authorization system can prevent a :term:`view` from being +invoked based on an :term:`authorization policy`. Before a view is invoked, the +authorization system can use the credentials in the :term:`request` along with +the :term:`context` resource to determine if access will be allowed. Here's +how it works at a high level: + +- A user may or may not have previously visited the application and supplied + authentication credentials, including a :term:`userid`. If so, the + application may have called :func:`pyramid.security.remember` to remember + these. - A :term:`request` is generated when a user visits the application. - Based on the request, a :term:`context` resource is located through :term:`resource location`. A context is located differently depending on - whether the application uses :term:`traversal` or :term:`URL dispatch`, but - a context is ultimately found in either case. See - the :ref:`urldispatch_chapter` chapter for more information. + whether the application uses :term:`traversal` or :term:`URL dispatch`, but a + context is ultimately found in either case. See the + :ref:`urldispatch_chapter` chapter for more information. -- A :term:`view callable` is located by :term:`view lookup` using the - context as well as other attributes of the request. +- A :term:`view callable` is located by :term:`view lookup` using the context + as well as other attributes of the request. -- If an :term:`authentication policy` is in effect, it is passed the - request; it returns some number of :term:`principal` identifiers. +- If an :term:`authentication policy` is in effect, it is passed the request. + It will return some number of :term:`principal` identifiers. To do this, the + policy would need to determine the authenticated :term:`userid` present in + the request. - If an :term:`authorization policy` is in effect and the :term:`view - configuration` associated with the view callable that was found has - a :term:`permission` associated with it, the authorization policy is - passed the :term:`context`, some number of :term:`principal` - identifiers returned by the authentication policy, and the - :term:`permission` associated with the view; it will allow or deny - access. - -- If the authorization policy allows access, the view callable is - invoked. - -- If the authorization policy denies access, the view callable is not - invoked; instead the :term:`forbidden view` is invoked. - -Security in :app:`Pyramid`, unlike many systems, cleanly and explicitly -separates authentication and authorization. Authentication is merely the -mechanism by which credentials provided in the :term:`request` are -resolved to one or more :term:`principal` identifiers. These identifiers -represent the users and groups in effect during the request. -Authorization then determines access based on the :term:`principal` -identifiers, the :term:`view callable` being invoked, and the -:term:`context` resource. + configuration` associated with the view callable that was found has a + :term:`permission` associated with it, the authorization policy is passed the + :term:`context`, some number of :term:`principal` identifiers returned by the + authentication policy, and the :term:`permission` associated with the view; + it will allow or deny access. + +- If the authorization policy allows access, the view callable is invoked. + +- If the authorization policy denies access, the view callable is not invoked. + Instead the :term:`forbidden view` is invoked. Authorization is enabled by modifying your application to include an -:term:`authentication policy` and :term:`authorization policy`. -:app:`Pyramid` comes with a variety of implementations of these -policies. To provide maximal flexibility, :app:`Pyramid` also -allows you to create custom authentication policies and authorization -policies. +:term:`authentication policy` and :term:`authorization policy`. :app:`Pyramid` +comes with a variety of implementations of these policies. To provide maximal +flexibility, :app:`Pyramid` also allows you to create custom authentication +policies and authorization policies. .. index:: single: authorization policy +.. _enabling_authorization_policy: + Enabling an Authorization Policy -------------------------------- -By default, :app:`Pyramid` enables no authorization policy. All -views are accessible by completely anonymous users. In order to begin -protecting views from execution based on security settings, you need -to enable an authorization policy. +:app:`Pyramid` does not enable any authorization policy by default. All views +are accessible by completely anonymous users. In order to begin protecting +views from execution based on security settings, you need to enable an +authorization policy. Enabling an Authorization Policy Imperatively ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Passing an ``authorization_policy`` argument to the constructor of the -:class:`~pyramid.config.Configurator` class enables an -authorization policy. +Use the :meth:`~pyramid.config.Configurator.set_authorization_policy` method of +the :class:`~pyramid.config.Configurator` to enable an authorization policy. -You must also enable an :term:`authentication policy` in order to -enable the authorization policy. This is because authorization, in -general, depends upon authentication. Use the -``authentication_policy`` argument to the -:class:`~pyramid.config.Configurator` class during -application setup to specify an authentication policy. +You must also enable an :term:`authentication policy` in order to enable the +authorization policy. This is because authorization, in general, depends upon +authentication. Use the +:meth:`~pyramid.config.Configurator.set_authentication_policy` method during +application setup to specify the authentication policy. For example: -.. ignore-next-block .. code-block:: python :linenos: from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authentication_policy = AuthTktAuthenticationPolicy('seekrit') - authorization_policy = ACLAuthorizationPolicy() - config = Configurator(authentication_policy=authentication_policy, - authorization_policy=authorization_policy) + authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config = Configurator() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) -.. note:: the ``authentication_policy`` and ``authorization_policy`` - arguments may also be passed to the Configurator as :term:`dotted - Python name` values, each representing the dotted name path to a - suitable implementation global defined at Python module scope. +.. note:: The ``authentication_policy`` and ``authorization_policy`` arguments + may also be passed to their respective methods mentioned above as + :term:`dotted Python name` values, each representing the dotted name path to + a suitable implementation global defined at Python module scope. The above configuration enables a policy which compares the value of an "auth ticket" cookie passed in the request's environment which contains a reference -to a single :term:`principal` against the principals present in any -:term:`ACL` found in the resource tree when attempting to call some -:term:`view`. +to a single :term:`userid`, and matches that userid's :term:`principals +<principal>` against the principals present in any :term:`ACL` found in the +resource tree when attempting to call some :term:`view`. While it is possible to mix and match different authentication and -authorization policies, it is an error to pass an authentication -policy without the authorization policy or vice versa to a -:term:`Configurator` constructor. +authorization policies, it is an error to configure a Pyramid application with +an authentication policy but without the authorization policy or vice versa. If +you do this, you'll receive an error at application startup time. + +.. seealso:: -See also the :mod:`pyramid.authorization` and -:mod:`pyramid.authentication` modules for alternate implementations -of authorization and authentication policies. + See also the :mod:`pyramid.authorization` and :mod:`pyramid.authentication` + modules for alternative implementations of authorization and authentication + policies. .. index:: single: permissions @@ -127,14 +132,13 @@ Protecting Views with Permissions To protect a :term:`view callable` from invocation based on a user's security settings when a particular type of resource becomes the :term:`context`, you -must pass a :term:`permission` to :term:`view configuration`. Permissions -are usually just strings, and they have no required composition: you can name +must pass a :term:`permission` to :term:`view configuration`. Permissions are +usually just strings, and they have no required composition: you can name permissions whatever you like. For example, the following view declaration protects the view named ``add_entry.html`` when the context resource is of type ``Blog`` with the -``add`` permission using the :meth:`pyramid.config.Configurator.add_view` -API: +``add`` permission using the :meth:`pyramid.config.Configurator.add_view` API: .. code-block:: python :linenos: @@ -146,10 +150,9 @@ API: context='mypackage.resources.Blog', permission='add') -The equivalent view registration including the ``add`` permission name -may be performed via the ``@view_config`` decorator: +The equivalent view registration including the ``add`` permission name may be +performed via the ``@view_config`` decorator: -.. ignore-next-block .. code-block:: python :linenos: @@ -162,34 +165,31 @@ may be performed via the ``@view_config`` decorator: pass As a result of any of these various view configuration statements, if an -authorization policy is in place when the view callable is found during -normal application operations, the requesting user will need to possess the -``add`` permission against the :term:`context` resource in order to be able -to invoke the ``blog_entry_add_view`` view. If he does not, the -:term:`Forbidden view` will be invoked. +authorization policy is in place when the view callable is found during normal +application operations, the requesting user will need to possess the ``add`` +permission against the :term:`context` resource in order to be able to invoke +the ``blog_entry_add_view`` view. If they do not, the :term:`Forbidden view` +will be invoked. + +.. index:: + pair: permission; default .. _setting_a_default_permission: Setting a Default Permission ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If a permission is not supplied to a view configuration, the registered -view will always be executable by entirely anonymous users: any -authorization policy in effect is ignored. +If a permission is not supplied to a view configuration, the registered view +will always be executable by entirely anonymous users: any authorization policy +in effect is ignored. -In support of making it easier to configure applications which are -"secure by default", :app:`Pyramid` allows you to configure a -*default* permission. If supplied, the default permission is used as -the permission string to all view registrations which don't otherwise -name a ``permission`` argument. +In support of making it easier to configure applications which are "secure by +default", :app:`Pyramid` allows you to configure a *default* permission. If +supplied, the default permission is used as the permission string to all view +registrations which don't otherwise name a ``permission`` argument. -These APIs are in support of configuring a default permission for an -application: - -- The ``default_permission`` constructor argument to the - :mod:`~pyramid.config.Configurator` constructor. - -- The :meth:`pyramid.config.Configurator.set_default_permission` method. +The :meth:`pyramid.config.Configurator.set_default_permission` method supports +configuring a default permission for an application. When a default permission is registered: @@ -197,9 +197,9 @@ When a default permission is registered: permission is ignored for that view registration, and the view-configuration-named permission is used. -- If a view configuration names an explicit permission as the string - ``__no_permission_required__``, the default permission is ignored, - and the view is registered *without* a permission (making it +- If a view configuration names the permission + :data:`pyramid.security.NO_PERMISSION_REQUIRED`, the default permission is + ignored, and the view is registered *without* a permission (making it available to all callers regardless of their credentials). .. warning:: @@ -207,33 +207,34 @@ When a default permission is registered: When you register a default permission, *all* views (even :term:`exception view` views) are protected by a permission. For all views which are truly meant to be anonymously accessible, you will need to associate the view's - configuration with the ``__no_permission_required__`` permission. + configuration with the :data:`pyramid.security.NO_PERMISSION_REQUIRED` + permission. .. index:: single: ACL single: access control list + pair: resource; ACL .. _assigning_acls: -Assigning ACLs to your Resource Objects +Assigning ACLs to Your Resource Objects --------------------------------------- -When the default :app:`Pyramid` :term:`authorization policy` determines -whether a user possesses a particular permission with respect to a resource, -it examines the :term:`ACL` associated with the resource. An ACL is -associated with a resource by adding an ``__acl__`` attribute to the resource -object. This attribute can be defined on the resource *instance* if you need +When the default :app:`Pyramid` :term:`authorization policy` determines whether +a user possesses a particular permission with respect to a resource, it +examines the :term:`ACL` associated with the resource. An ACL is associated +with a resource by adding an ``__acl__`` attribute to the resource object. +This attribute can be defined on the resource *instance* if you need instance-level security, or it can be defined on the resource *class* if you just need type-level security. -For example, an ACL might be attached to the resource for a blog via its -class: +For example, an ACL might be attached to the resource for a blog via its class: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): __acl__ = [ @@ -248,8 +249,8 @@ Or, if your resources are persistent, an ACL might be specified via the .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone class Blog(object): pass @@ -262,11 +263,32 @@ Or, if your resources are persistent, an ACL might be specified via the (Allow, 'group:editors', 'edit'), ] -Whether an ACL is attached to a resource's class or an instance of the -resource itself, the effect is the same. It is useful to decorate individual -resource instances with an ACL (as opposed to just decorating their class) in -applications such as "CMS" systems where fine-grained access is required on -an object-by-object basis. +Whether an ACL is attached to a resource's class or an instance of the resource +itself, the effect is the same. It is useful to decorate individual resource +instances with an ACL (as opposed to just decorating their class) in +applications such as content management systems where fine-grained access is +required on an object-by-object basis. + +Dynamic ACLs are also possible by turning the ACL into a callable on the +resource. This may allow the ACL to dynamically generate rules based on +properties of the instance. + +.. code-block:: python + :linenos: + + from pyramid.security import Allow + from pyramid.security import Everyone + + class Blog(object): + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, self.owner, 'edit'), + (Allow, 'group:editors', 'edit'), + ] + + def __init__(self, owner): + self.owner = owner .. index:: single: ACE @@ -280,8 +302,8 @@ Here's an example ACL: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -289,49 +311,43 @@ Here's an example ACL: (Allow, 'group:editors', 'edit'), ] -The example ACL indicates that the -:data:`pyramid.security.Everyone` principal -- a special -system-defined principal indicating, literally, everyone -- is allowed -to view the blog, the ``group:editors`` principal is allowed to add to -and edit the blog. +The example ACL indicates that the :data:`pyramid.security.Everyone` +principal—a special system-defined principal indicating, literally, everyone—is +allowed to view the blog, and the ``group:editors`` principal is allowed to add +to and edit the blog. -Each element of an ACL is an :term:`ACE` or access control entry. -For example, in the above code block, there are three ACEs: ``(Allow, -Everyone, 'view')``, ``(Allow, 'group:editors', 'add')``, and -``(Allow, 'group:editors', 'edit')``. +Each element of an ACL is an :term:`ACE`, or access control entry. For example, +in the above code block, there are three ACEs: ``(Allow, Everyone, 'view')``, +``(Allow, 'group:editors', 'add')``, and ``(Allow, 'group:editors', 'edit')``. -The first element of any ACE is either -:data:`pyramid.security.Allow`, or -:data:`pyramid.security.Deny`, representing the action to take when -the ACE matches. The second element is a :term:`principal`. The -third argument is a permission or sequence of permission names. +The first element of any ACE is either :data:`pyramid.security.Allow`, or +:data:`pyramid.security.Deny`, representing the action to take when the ACE +matches. The second element is a :term:`principal`. The third argument is a +permission or sequence of permission names. A principal is usually a user id, however it also may be a group id if your authentication system provides group information and the effective :term:`authentication policy` policy is written to respect group information. -For example, the -:class:`pyramid.authentication.RepozeWho1AuthenicationPolicy` respects group -information if you configure it with a ``callback``. +See :ref:`extending_default_authentication_policies`. -Each ACE in an ACL is processed by an authorization policy *in the -order dictated by the ACL*. So if you have an ACL like this: +Each ACE in an ACL is processed by an authorization policy *in the order +dictated by the ACL*. So if you have an ACL like this: .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow from pyramid.security import Deny + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), (Deny, Everyone, 'view'), ] -The default authorization policy will *allow* everyone the view -permission, even though later in the ACL you have an ACE that denies -everyone the view permission. On the other hand, if you have an ACL -like this: +The default authorization policy will *allow* everyone the view permission, +even though later in the ACL you have an ACE that denies everyone the view +permission. On the other hand, if you have an ACL like this: .. code-block:: python :linenos: @@ -345,20 +361,19 @@ like this: (Allow, Everyone, 'view'), ] -The authorization policy will deny everyone the view permission, even -though later in the ACL is an ACE that allows everyone. +The authorization policy will deny everyone the view permission, even though +later in the ACL, there is an ACE that allows everyone. -The third argument in an ACE can also be a sequence of permission -names instead of a single permission name. So instead of creating -multiple ACEs representing a number of different permission grants to -a single ``group:editors`` group, we can collapse this into a single -ACE, as below. +The third argument in an ACE can also be a sequence of permission names instead +of a single permission name. So instead of creating multiple ACEs representing +a number of different permission grants to a single ``group:editors`` group, we +can collapse this into a single ACE, as below. .. code-block:: python :linenos: - from pyramid.security import Everyone from pyramid.security import Allow + from pyramid.security import Everyone __acl__ = [ (Allow, Everyone, 'view'), @@ -373,23 +388,21 @@ ACE, as below. Special Principal Names ----------------------- -Special principal names exist in the :mod:`pyramid.security` -module. They can be imported for use in your own code to populate -ACLs, e.g. :data:`pyramid.security.Everyone`. +Special principal names exist in the :mod:`pyramid.security` module. They can +be imported for use in your own code to populate ACLs, e.g., +:data:`pyramid.security.Everyone`. :data:`pyramid.security.Everyone` - Literally, everyone, no matter what. This object is actually a - string "under the hood" (``system.Everyone``). Every user "is" the - principal named Everyone during every request, even if a security - policy is not in use. + Literally, everyone, no matter what. This object is actually a string under + the hood (``system.Everyone``). Every user *is* the principal named + "Everyone" during every request, even if a security policy is not in use. :data:`pyramid.security.Authenticated` - Any user with credentials as determined by the current security - policy. You might think of it as any user that is "logged in". - This object is actually a string "under the hood" - (``system.Authenticated``). + Any user with credentials as determined by the current security policy. You + might think of it as any user that is "logged in". This object is actually a + string under the hood (``system.Authenticated``). .. index:: single: permission names @@ -398,19 +411,19 @@ ACLs, e.g. :data:`pyramid.security.Everyone`. Special Permissions ------------------- -Special permission names exist in the :mod:`pyramid.security` -module. These can be imported for use in ACLs. +Special permission names exist in the :mod:`pyramid.security` module. These +can be imported for use in ACLs. .. _all_permissions: :data:`pyramid.security.ALL_PERMISSIONS` - An object representing, literally, *all* permissions. Useful in an - ACL like so: ``(Allow, 'fred', ALL_PERMISSIONS)``. The - ``ALL_PERMISSIONS`` object is actually a stand-in object that has a - ``__contains__`` method that always returns ``True``, which, for all - known authorization policies, has the effect of indicating that a - given principal "has" any permission asked for by the system. + An object representing, literally, *all* permissions. Useful in an ACL like + so: ``(Allow, 'fred', ALL_PERMISSIONS)``. The ``ALL_PERMISSIONS`` object is + actually a stand-in object that has a ``__contains__`` method that always + returns ``True``, which, for all known authorization policies, has the effect + of indicating that a given principal has any permission asked for by the + system. .. index:: single: special ACE @@ -421,11 +434,11 @@ Special ACEs A convenience :term:`ACE` is defined representing a deny to everyone of all permissions in :data:`pyramid.security.DENY_ALL`. This ACE is often used as -the *last* ACE of an ACL to explicitly cause inheriting authorization -policies to "stop looking up the traversal tree" (effectively breaking any -inheritance). For example, an ACL which allows *only* ``fred`` the view -permission for a particular resource despite what inherited ACLs may say when -the default authorization policy is in effect might look like so: +the *last* ACE of an ACL to explicitly cause inheriting authorization policies +to "stop looking up the traversal tree" (effectively breaking any inheritance). +For example, an ACL which allows *only* ``fred`` the view permission for a +particular resource, despite what inherited ACLs may say when the default +authorization policy is in effect, might look like so: .. code-block:: python :linenos: @@ -435,8 +448,8 @@ the default authorization policy is in effect might look like so: __acl__ = [ (Allow, 'fred', 'view'), DENY_ALL ] -"Under the hood", the :data:`pyramid.security.DENY_ALL` ACE equals -the following: +Under the hood, the :data:`pyramid.security.DENY_ALL` ACE equals the +following: .. code-block:: python :linenos: @@ -453,14 +466,14 @@ ACL Inheritance and Location-Awareness While the default :term:`authorization policy` is in place, if a resource object does not have an ACL when it is the context, its *parent* is consulted -for an ACL. If that object does not have an ACL, *its* parent is consulted -for an ACL, ad infinitum, until we've reached the root and there are no more +for an ACL. If that object does not have an ACL, *its* parent is consulted for +an ACL, ad infinitum, until we've reached the root and there are no more parents left. In order to allow the security machinery to perform ACL inheritance, resource objects must provide *location-awareness*. Providing *location-awareness* -means two things: the root object in the resource tree must have a -``__name__`` attribute and a ``__parent__`` attribute. +means two things: the root object in the resource tree must have a ``__name__`` +attribute and a ``__parent__`` attribute. .. code-block:: python :linenos: @@ -469,13 +482,19 @@ means two things: the root object in the resource tree must have a __name__ = '' __parent__ = None -An object with a ``__parent__`` attribute and a ``__name__`` attribute -is said to be *location-aware*. Location-aware objects define an -``__parent__`` attribute which points at their parent object. The -root object's ``__parent__`` is ``None``. +An object with a ``__parent__`` attribute and a ``__name__`` attribute is said +to be *location-aware*. Location-aware objects define a ``__parent__`` +attribute which points at their parent object. The root object's +``__parent__`` is ``None``. + +.. seealso:: -See :ref:`location_module` for documentations of functions which use -location-awareness. See also :ref:`location_aware`. + See also :ref:`location_module` for documentations of functions which use + location-awareness. + +.. seealso:: + + See also :ref:`location_aware`. .. index:: single: forbidden view @@ -483,12 +502,11 @@ location-awareness. See also :ref:`location_aware`. Changing the Forbidden View --------------------------- -When :app:`Pyramid` denies a view invocation due to an -authorization denial, the special ``forbidden`` view is invoked. "Out -of the box", this forbidden view is very plain. See -:ref:`changing_the_forbidden_view` within :ref:`hooks_chapter` for -instructions on how to create a custom forbidden view and arrange for -it to be called when view authorization is denied. +When :app:`Pyramid` denies a view invocation due to an authorization denial, +the special ``forbidden`` view is invoked. Out of the box, this forbidden view +is very plain. See :ref:`changing_the_forbidden_view` within +:ref:`hooks_chapter` for instructions on how to create a custom forbidden view +and arrange for it to be called when view authorization is denied. .. index:: single: debugging authorization failures @@ -498,51 +516,101 @@ it to be called when view authorization is denied. Debugging View Authorization Failures ------------------------------------- -If your application in your judgment is allowing or denying view -access inappropriately, start your application under a shell using the +If your application in your judgment is allowing or denying view access +inappropriately, start your application under a shell using the ``PYRAMID_DEBUG_AUTHORIZATION`` environment variable set to ``1``. For example: .. code-block:: text - $ PYRAMID_DEBUG_AUTHORIZATION=1 bin/paster serve myproject.ini + $ PYRAMID_DEBUG_AUTHORIZATION=1 $VENV/bin/pserve myproject.ini -When any authorization takes place during a top-level view rendering, -a message will be logged to the console (to stderr) about what ACE in -which ACL permitted or denied the authorization based on -authentication information. +When any authorization takes place during a top-level view rendering, a message +will be logged to the console (to stderr) about what ACE in which ACL permitted +or denied the authorization based on authentication information. -This behavior can also be turned on in the application ``.ini`` file -by setting the ``debug_authorization`` key to ``true`` within the -application's configuration section, e.g.: +This behavior can also be turned on in the application ``.ini`` file by setting +the ``pyramid.debug_authorization`` key to ``true`` within the application's +configuration section, e.g.: .. code-block:: ini :linenos: [app:main] - use = egg:MyProject#app - debug_authorization = true + use = egg:MyProject + pyramid.debug_authorization = true -With this debug flag turned on, the response sent to the browser will -also contain security debugging information in its body. +With this debug flag turned on, the response sent to the browser will also +contain security debugging information in its body. Debugging Imperative Authorization Failures ------------------------------------------- -The :func:`pyramid.security.has_permission` API is used to check -security within view functions imperatively. It returns instances of -objects that are effectively booleans. But these objects are not raw -``True`` or ``False`` objects, and have information attached to them -about why the permission was allowed or denied. The object will be -one of :data:`pyramid.security.ACLAllowed`, -:data:`pyramid.security.ACLDenied`, -:data:`pyramid.security.Allowed`, or -:data:`pyramid.security.Denied`, as documented in -:ref:`security_module`. At the very minimum these objects will have a -``msg`` attribute, which is a string indicating why the permission was -denied or allowed. Introspecting this information in the debugger or -via print statements when a call to -:func:`~pyramid.security.has_permission` fails is often useful. +The :meth:`pyramid.request.Request.has_permission` API is used to check +security within view functions imperatively. It returns instances of objects +that are effectively booleans. But these objects are not raw ``True`` or +``False`` objects, and have information attached to them about why the +permission was allowed or denied. The object will be one of +:data:`pyramid.security.ACLAllowed`, :data:`pyramid.security.ACLDenied`, +:data:`pyramid.security.Allowed`, or :data:`pyramid.security.Denied`, as +documented in :ref:`security_module`. At the very minimum, these objects will +have a ``msg`` attribute, which is a string indicating why the permission was +denied or allowed. Introspecting this information in the debugger or via print +statements when a call to :meth:`~pyramid.request.Request.has_permission` fails +is often useful. + +.. index:: + single: authentication policy (extending) + +.. _extending_default_authentication_policies: + +Extending Default Authentication Policies +----------------------------------------- + +Pyramid ships with some built in authentication policies for use in your +applications. See :mod:`pyramid.authentication` for the available policies. +They differ on their mechanisms for tracking authentication credentials between +requests, however they all interface with your application in mostly the same +way. + +Above you learned about :ref:`assigning_acls`. Each :term:`principal` used in +the :term:`ACL` is matched against the list returned from +:meth:`pyramid.interfaces.IAuthenticationPolicy.effective_principals`. +Similarly, :meth:`pyramid.request.Request.authenticated_userid` maps to +:meth:`pyramid.interfaces.IAuthenticationPolicy.authenticated_userid`. + +You may control these values by subclassing the default authentication +policies. For example, below we subclass the +:class:`pyramid.authentication.AuthTktAuthenticationPolicy` and define extra +functionality to query our database before confirming that the :term:`userid` +is valid in order to avoid blindly trusting the value in the cookie (what if +the cookie is still valid, but the user has deleted their account?). We then +use that :term:`userid` to augment the ``effective_principals`` with +information about groups and other state for that user. + +.. code-block:: python + :linenos: + + from pyramid.authentication import AuthTktAuthenticationPolicy + + class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + userid = self.unauthenticated_userid(request) + if userid: + if request.verify_userid_is_still_valid(userid): + return userid + + def effective_principals(self, request): + principals = [Everyone] + userid = self.authenticated_userid(request) + if userid: + principals += [Authenticated, str(userid)] + return principals + +In most instances ``authenticated_userid`` and ``effective_principals`` are +application-specific, whereas ``unauthenticated_userid``, ``remember``, and +``forget`` are generic and focused on transport and serialization of data +between consecutive requests. .. index:: single: authentication policy (creating) @@ -552,54 +620,70 @@ via print statements when a call to Creating Your Own Authentication Policy --------------------------------------- -:app:`Pyramid` ships with a number of useful out-of-the-box -security policies (see :mod:`pyramid.authentication`). However, -creating your own authentication policy is often necessary when you -want to control the "horizontal and vertical" of how your users -authenticate. Doing so is a matter of creating an instance of something -that implements the following interface: +:app:`Pyramid` ships with a number of useful out-of-the-box security policies +(see :mod:`pyramid.authentication`). However, creating your own authentication +policy is often necessary when you want to control the "horizontal and +vertical" of how your users authenticate. Doing so is a matter of creating an +instance of something that implements the following interface: .. code-block:: python :linenos: - class AuthenticationPolicy(object): + class IAuthenticationPolicy(object): """ An object representing a Pyramid authentication policy. """ def authenticated_userid(self, request): - """ Return the authenticated userid or ``None`` if no - authenticated userid can be found. This method of the policy - should ensure that a record exists in whatever persistent store is - used related to the user (the user should not have been deleted); - if a record associated with the current id does not exist in a - persistent store, it should return ``None``.""" + """ Return the authenticated :term:`userid` or ``None`` if + no authenticated userid can be found. This method of the + policy should ensure that a record exists in whatever + persistent store is used related to the user (the user + should not have been deleted); if a record associated with + the current id does not exist in a persistent store, it + should return ``None``. + + """ def unauthenticated_userid(self, request): - """ Return the *unauthenticated* userid. This method performs the - same duty as ``authenticated_userid`` but is permitted to return the - userid based only on data present in the request; it needn't (and - shouldn't) check any persistent store to ensure that the user record - related to the request userid exists.""" + """ Return the *unauthenticated* userid. This method + performs the same duty as ``authenticated_userid`` but is + permitted to return the userid based only on data present + in the request; it needn't (and shouldn't) check any + persistent store to ensure that the user record related to + the request userid exists. + + This method is intended primarily a helper to assist the + ``authenticated_userid`` method in pulling credentials out + of the request data, abstracting away the specific headers, + query strings, etc that are used to authenticate the request. + + """ def effective_principals(self, request): """ Return a sequence representing the effective principals - including the userid and any groups belonged to by the current - user, including 'system' groups such as - ``pyramid.security.Everyone`` and - ``pyramid.security.Authenticated``. """ + typically including the :term:`userid` and any groups belonged + to by the current user, always including 'system' groups such + as ``pyramid.security.Everyone`` and + ``pyramid.security.Authenticated``. + + """ - def remember(self, request, principal, **kw): + def remember(self, request, userid, **kw): """ Return a set of headers suitable for 'remembering' the - principal named ``principal`` when set in a response. An - individual authentication policy and its consumers can decide - on the composition and meaning of **kw. """ - + :term:`userid` named ``userid`` when set in a response. An + individual authentication policy and its consumers can + decide on the composition and meaning of **kw. + + """ + def forget(self, request): """ Return a set of headers suitable for 'forgetting' the - current user on subsequent requests. """ + current user on subsequent requests. + + """ After you do so, you can pass an instance of such a class into the -:class:`~pyramid.config.Configurator` class at configuration -time as ``authentication_policy`` to use it. +:class:`~pyramid.config.Configurator.set_authentication_policy` method at +configuration time to use it. .. index:: single: authorization policy (creating) @@ -609,24 +693,20 @@ time as ``authentication_policy`` to use it. Creating Your Own Authorization Policy -------------------------------------- -An authorization policy is a policy that allows or denies access after -a user has been authenticated. By default, :app:`Pyramid` will use -the :class:`pyramid.authorization.ACLAuthorizationPolicy` if an -authentication policy is activated and an authorization policy isn't -otherwise specified. - -In some cases, it's useful to be able to use a different -authorization policy than the default -:class:`~pyramid.authorization.ACLAuthorizationPolicy`. For -example, it might be desirable to construct an alternate authorization -policy which allows the application to use an authorization mechanism -that does not involve :term:`ACL` objects. - -:app:`Pyramid` ships with only a single default authorization -policy, so you'll need to create your own if you'd like to use a -different one. Creating and using your own authorization policy is a -matter of creating an instance of an object that implements the -following interface: +An authorization policy is a policy that allows or denies access after a user +has been authenticated. Most :app:`Pyramid` applications will use the default +:class:`pyramid.authorization.ACLAuthorizationPolicy`. + +However, in some cases, it's useful to be able to use a different authorization +policy than the default :class:`~pyramid.authorization.ACLAuthorizationPolicy`. +For example, it might be desirable to construct an alternate authorization +policy which allows the application to use an authorization mechanism that does +not involve :term:`ACL` objects. + +:app:`Pyramid` ships with only a single default authorization policy, so you'll +need to create your own if you'd like to use a different one. Creating and +using your own authorization policy is a matter of creating an instance of an +object that implements the following interface: .. code-block:: python :linenos: @@ -648,5 +728,32 @@ following interface: used.""" After you do so, you can pass an instance of such a class into the -:class:`~pyramid.config.Configurator` class at configuration -time as ``authorization_policy`` to use it. +:class:`~pyramid.config.Configurator.set_authorization_policy` method at +configuration time to use it. + +.. _admonishment_against_secret_sharing: + +Admonishment Against Secret-Sharing +----------------------------------- + +A "secret" is required by various components of Pyramid. For example, the +:term:`authentication policy` below uses a secret value ``seekrit``:: + + authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512') + +A :term:`session factory` also requires a secret:: + + my_session_factory = SignedCookieSessionFactory('itsaseekreet') + +It is tempting to use the same secret for multiple Pyramid subsystems. For +example, you might be tempted to use the value ``seekrit`` as the secret for +both the authentication policy and the session factory defined above. This is +a bad idea, because in both cases, these secrets are used to sign the payload +of the data. + +If you use the same secret for two different parts of your application for +signing purposes, it may allow an attacker to get his chosen plaintext signed, +which would allow the attacker to control the content of the payload. Re-using +a secret across two different subsystems might drop the security of signing to +zero. Keys should not be re-used across different contexts where an attacker +has the possibility of providing a chosen plaintext. diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 97e3ebc55..7cf96ac7d 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -6,72 +6,80 @@ Sessions ======== -A :term:`session` is a namespace which is valid for some period of -continual activity that can be used to represent a user's interaction -with a web application. +A :term:`session` is a namespace which is valid for some period of continual +activity that can be used to represent a user's interaction with a web +application. -This chapter describes how to configure sessions, what session -implementations :app:`Pyramid` provides out of the box, how to store and -retrieve data from sessions, and two session-specific features: flash -messages, and cross-site request forgery attack prevention. +This chapter describes how to configure sessions, what session implementations +:app:`Pyramid` provides out of the box, how to store and retrieve data from +sessions, and two session-specific features: flash messages, and cross-site +request forgery attack prevention. + +.. index:: + single: session factory (default) .. _using_the_default_session_factory: -Using The Default Session Factory +Using the Default Session Factory --------------------------------- -In order to use sessions, you must set up a :term:`session factory` -during your :app:`Pyramid` configuration. +In order to use sessions, you must set up a :term:`session factory` during your +:app:`Pyramid` configuration. -A very basic, insecure sample session factory implementation is -provided in the :app:`Pyramid` core. It uses a cookie to store -session information. This implementation has the following -limitation: +A very basic, insecure sample session factory implementation is provided in the +:app:`Pyramid` core. It uses a cookie to store session information. This +implementation has the following limitations: -- The session information in the cookies used by this implementation - is *not* encrypted, so it can be viewed by anyone with access to the - cookie storage of the user's browser or anyone with access to the - network along which the cookie travels. +- The session information in the cookies used by this implementation is *not* + encrypted, so it can be viewed by anyone with access to the cookie storage of + the user's browser or anyone with access to the network along which the + cookie travels. -- The maximum number of bytes that are storable in a serialized - representation of the session is fewer than 4000. This is - suitable only for very small data sets. +- The maximum number of bytes that are storable in a serialized representation + of the session is fewer than 4000. This is suitable only for very small data + sets. -It is digitally signed, however, and thus its data cannot easily be -tampered with. +It is digitally signed, however, and thus its data cannot easily be tampered +with. -You can configure this session factory in your :app:`Pyramid` -application by using the ``session_factory`` argument to the -:class:`~pyramid.config.Configurator` class: +You can configure this session factory in your :app:`Pyramid` application by +using the :meth:`pyramid.config.Configurator.set_session_factory` method. .. code-block:: python :linenos: - from pyramid.session import UnencryptedCookieSessionFactoryConfig - my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet') - - from pyramid.config import Configurator - config = Configurator(session_factory = my_session_factory) - -.. warning:: + from pyramid.session import SignedCookieSessionFactory + my_session_factory = SignedCookieSessionFactory('itsaseekreet') - Note the very long, very explicit name for - ``UnencryptedCookieSessionFactoryConfig``. It's trying to tell you that - this implementation is, by default, *unencrypted*. You should not use it - when you keep sensitive information in the session object, as the - information can be easily read by both users of your application and third - parties who have access to your users' network traffic. Use a different + from pyramid.config import Configurator + config = Configurator() + config.set_session_factory(my_session_factory) + +.. warning:: + + By default the :func:`~pyramid.session.SignedCookieSessionFactory` + implementation is *unencrypted*. You should not use it when you keep + sensitive information in the session object, as the information can be + easily read by both users of your application and third parties who have + access to your users' network traffic. And, if you use this sessioning + implementation, and you inadvertently create a cross-site scripting + vulnerability in your application, because the session data is stored + unencrypted in a cookie, it will also be easier for evildoers to obtain the + current user's cross-site scripting token. In short, use a different session factory implementation (preferably one which keeps session data on the server) for anything but the most basic of applications where "session - security doesn't matter". + security doesn't matter", and you are sure your application has no + cross-site scripting vulnerabilities. + +.. index:: + single: session object Using a Session Object ---------------------- -Once a session factory has been configured for your application, you -can access session objects provided by the session factory via -the ``session`` attribute of any :term:`request` object. For -example: +Once a session factory has been configured for your application, you can access +session objects provided by the session factory via the ``session`` attribute +of any :term:`request` object. For example: .. code-block:: python :linenos: @@ -88,8 +96,12 @@ example: else: return Response('Fred was not in the session') +The first time this view is invoked produces ``Fred was not in the session``. +Subsequent invocations produce ``Fred was in the session``, assuming of course +that the client side maintains the session's identity across multiple requests. + You can use a session much like a Python dictionary. It supports all -dictionary methods, along with some extra attributes, and methods. +dictionary methods, along with some extra attributes and methods. Extra attributes: @@ -97,79 +109,87 @@ Extra attributes: An integer timestamp indicating the time that this session was created. ``new`` - A boolean. If ``new`` is True, this session is new. Otherwise, it has - been constituted from data that was already serialized. + A boolean. If ``new`` is True, this session is new. Otherwise, it has been + constituted from data that was already serialized. Extra methods: ``changed()`` - Call this when you mutate a mutable value in the session namespace. - See the gotchas below for details on when, and why you should - call this. + Call this when you mutate a mutable value in the session namespace. See the + gotchas below for details on when and why you should call this. ``invalidate()`` - Call this when you want to invalidate the session (dump all data, - and -- perhaps -- set a clearing cookie). + Call this when you want to invalidate the session (dump all data, and perhaps + set a clearing cookie). -The formal definition of the methods and attributes supported by the -session object are in the :class:`pyramid.interfaces.ISession` -documentation. +The formal definition of the methods and attributes supported by the session +object are in the :class:`pyramid.interfaces.ISession` documentation. Some gotchas: -- Keys and values of session data must be *pickleable*. This means, - typically, that they are instances of basic types of objects, - such as strings, lists, dictionaries, tuples, integers, etc. If you - place an object in a session data key or value that is not - pickleable, an error will be raised when the session is serialized. - -- If you place a mutable value (for example, a list or a dictionary) - in a session object, and you subsequently mutate that value, you must - call the ``changed()`` method of the session object. In this case, the - session has no way to know that is was modified. However, when you - modify a session object directly, such as setting a value (i.e., - ``__setitem__``), or removing a key (e.g., ``del`` or ``pop``), the - session will automatically know that it needs to re-serialize its - data, thus calling ``changed()`` is unnecessary. There is no harm in - calling ``changed()`` in either case, so when in doubt, call it after - you've changed sessioning data. +- Keys and values of session data must be *pickleable*. This means, typically, + that they are instances of basic types of objects, such as strings, lists, + dictionaries, tuples, integers, etc. If you place an object in a session + data key or value that is not pickleable, an error will be raised when the + session is serialized. + +- If you place a mutable value (for example, a list or a dictionary) in a + session object, and you subsequently mutate that value, you must call the + ``changed()`` method of the session object. In this case, the session has no + way to know that it was modified. However, when you modify a session object + directly, such as setting a value (i.e., ``__setitem__``), or removing a key + (e.g., ``del`` or ``pop``), the session will automatically know that it needs + to re-serialize its data, thus calling ``changed()`` is unnecessary. There is + no harm in calling ``changed()`` in either case, so when in doubt, call it + after you've changed sessioning data. .. index:: - single: pyramid_beaker - single: Beaker + single: pyramid_redis_sessions + single: session factory (alternates) .. _using_alternate_session_factories: Using Alternate Session Factories --------------------------------- -At the time of this writing, exactly one alternate session factory -implementation exists, named ``pyramid_beaker``. This is a session -factory that uses the `Beaker <http://beaker.groovie.org/>`_ library -as a backend. Beaker has support for file-based sessions, database -based sessions, and encrypted cookie-based sessions. See -`http://github.com/Pylons/pyramid_beaker -<http://github.com/Pylons/pyramid_beaker>`_ for more information about -``pyramid_beaker``. +The following session factories exist at the time of this writing. + +======================= ======= ============================= +Session Factory Backend Description +======================= ======= ============================= +pyramid_redis_sessions_ Redis_ Server-side session library + for Pyramid, using Redis for + storage. +pyramid_beaker_ Beaker_ Session factory for Pyramid + backed by the Beaker + sessioning system. +======================= ======= ============================= + +.. _pyramid_redis_sessions: https://pypi.python.org/pypi/pyramid_redis_sessions +.. _Redis: http://redis.io/ + +.. _pyramid_beaker: https://pypi.python.org/pypi/pyramid_beaker +.. _Beaker: http://beaker.readthedocs.org/en/latest/ .. index:: - single: session factory + single: session factory (custom) Creating Your Own Session Factory --------------------------------- -If none of the default or otherwise available sessioning -implementations for :app:`Pyramid` suit you, you may create your own -session object by implementing a :term:`session factory`. Your -session factory should return a :term:`session`. The interfaces for -both types are available in +If none of the default or otherwise available sessioning implementations for +:app:`Pyramid` suit you, you may create your own session object by implementing +a :term:`session factory`. Your session factory should return a +:term:`session`. The interfaces for both types are available in :class:`pyramid.interfaces.ISessionFactory` and -:class:`pyramid.interfaces.ISession`. You might use the cookie -implementation in the :mod:`pyramid.session` module as inspiration. +:class:`pyramid.interfaces.ISession`. You might use the cookie implementation +in the :mod:`pyramid.session` module as inspiration. .. index:: single: flash messages +.. _flash_messages: + Flash Messages -------------- @@ -178,12 +198,15 @@ Flash Messages factory` as described in :ref:`using_the_default_session_factory` or :ref:`using_alternate_session_factories`. -Flash messaging has two main uses: to display a status message only once to -the user after performing an internal redirect, and to allow generic code to -log messages for single-time display without having direct access to an HTML +Flash messaging has two main uses: to display a status message only once to the +user after performing an internal redirect, and to allow generic code to log +messages for single-time display without having direct access to an HTML template. The user interface consists of a number of methods of the :term:`session` object. +.. index:: + single: session.flash + Using the ``session.flash`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -195,7 +218,7 @@ method: request.session.flash('mymessage') The ``flash()`` method appends a message to a flash queue, creating the queue -if necessary. +if necessary. ``flash()`` accepts three arguments: @@ -205,78 +228,74 @@ The ``message`` argument is required. It represents a message you wish to later display to a user. It is usually a string but the ``message`` you provide is not modified in any way. -The ``queue`` argument allows you to choose a queue to which to append -the message you provide. This can be used to push different kinds of -messages into flash storage for later display in different places on a -page. You can pass any name for your queue, but it must be a string. -Each queue is independent, and can be popped by ``pop_flash()`` or -examined via ``peek_flash()`` separately. ``queue`` defaults to the -empty string. The empty string represents the default flash message -queue. +The ``queue`` argument allows you to choose a queue to which to append the +message you provide. This can be used to push different kinds of messages into +flash storage for later display in different places on a page. You can pass +any name for your queue, but it must be a string. Each queue is independent, +and can be popped by ``pop_flash()`` or examined via ``peek_flash()`` +separately. ``queue`` defaults to the empty string. The empty string +represents the default flash message queue. .. code-block:: python request.session.flash(msg, 'myappsqueue') -The ``allow_duplicate`` argument defaults to ``True``. If this is -``False``, and you attempt to add a message value which is already -present in the queue, it will not be added. +The ``allow_duplicate`` argument defaults to ``True``. If this is ``False``, +and you attempt to add a message value which is already present in the queue, +it will not be added. + +.. index:: + single: session.pop_flash Using the ``session.pop_flash`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once one or more messages have been added to a flash queue by the -``session.flash()`` API, the ``session.pop_flash()`` API can be used to -pop an entire queue and return it for use. +``session.flash()`` API, the ``session.pop_flash()`` API can be used to pop an +entire queue and return it for use. To pop a particular queue of messages from the flash object, use the session -object's ``pop_flash()`` method. This returns a list of the messages -that were added to the flash queue, and empties the queue. +object's ``pop_flash()`` method. This returns a list of the messages that were +added to the flash queue, and empties the queue. .. method:: pop_flash(queue='') -.. code-block:: python - :linenos: - - >>> request.session.flash('info message') - >>> request.session.pop_flash() - ['info message'] +>>> request.session.flash('info message') +>>> request.session.pop_flash() +['info message'] Calling ``session.pop_flash()`` again like above without a corresponding call to ``session.flash()`` will return an empty list, because the queue has already been popped. -.. code-block:: python - :linenos: +>>> request.session.flash('info message') +>>> request.session.pop_flash() +['info message'] +>>> request.session.pop_flash() +[] - >>> request.session.flash('info message') - >>> request.session.pop_flash() - ['info message'] - >>> request.session.pop_flash() - [] +.. index:: + single: session.peek_flash Using the ``session.peek_flash`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Once one or more messages has been added to a flash queue by the -``session.flash()`` API, the ``session.peek_flash()`` API can be used to -"peek" at that queue. Unlike ``session.pop_flash()``, the queue is not -popped from flash storage. +Once one or more messages have been added to a flash queue by the +``session.flash()`` API, the ``session.peek_flash()`` API can be used to "peek" +at that queue. Unlike ``session.pop_flash()``, the queue is not popped from +flash storage. .. method:: peek_flash(queue='') -.. code-block:: python - :linenos: - - >>> request.session.flash('info message') - >>> request.session.peek_flash() - ['info message'] - >>> request.session.peek_flash() - ['info message'] - >>> request.session.pop_flash() - ['info message'] - >>> request.session.peek_flash() - [] +>>> request.session.flash('info message') +>>> request.session.peek_flash() +['info message'] +>>> request.session.peek_flash() +['info message'] +>>> request.session.pop_flash() +['info message'] +>>> request.session.peek_flash() +[] .. index:: single: preventing cross-site request forgery attacks @@ -287,17 +306,21 @@ Preventing Cross-Site Request Forgery Attacks `Cross-site request forgery <http://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a -phenomenon whereby a user with an identity on your website might click on a -URL or button on another website which unwittingly redirects the user to your -application to perform some command that requires elevated privileges. - -You can avoid most of these attacks by making sure that the correct *CSRF -token* has been set in an :app:`Pyramid` session object before performing any -actions in code which requires elevated privileges that is invoked via a form -post. To use CSRF token support, you must enable a :term:`session factory` -as described in :ref:`using_the_default_session_factory` or +phenomenon whereby a user who is logged in to your website might inadvertantly +load a URL because it is linked from, or embedded in, an attacker's website. +If the URL is one that may modify or delete data, the consequences can be dire. + +You can avoid most of these attacks by issuing a unique token to the browser +and then requiring that it be present in all potentially unsafe requests. +:app:`Pyramid` sessions provide facilities to create and check CSRF tokens. + +To use CSRF tokens, you must first enable a :term:`session factory` as +described in :ref:`using_the_default_session_factory` or :ref:`using_alternate_session_factories`. +.. index:: + single: session.get_csrf_token + Using the ``session.get_csrf_token`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -310,37 +333,142 @@ To get the current CSRF token from the session, use the The ``session.get_csrf_token()`` method accepts no arguments. It returns a CSRF *token* string. If ``session.get_csrf_token()`` or -``session.new_csrf_token()`` was invoked previously for this session, the -existing token will be returned. If no CSRF token previously existed for -this session, a new token will be will be set into the session and returned. -The newly created token will be opaque and randomized. +``session.new_csrf_token()`` was invoked previously for this session, then the +existing token will be returned. If no CSRF token previously existed for this +session, then a new token will be set into the session and returned. The newly +created token will be opaque and randomized. You can use the returned token as the value of a hidden field in a form that -posts to a method that requires elevated privileges. The handler for the -form post should use ``session.get_csrf_token()`` *again* to obtain the -current CSRF token related to the user from the session, and compare it to -the value of the hidden form field. For example, if your form rendering -included the CSRF token obtained via ``session.get_csrf_token()`` as a hidden -input field named ``csrf_token``: +posts to a method that requires elevated privileges, or supply it as a request +header in AJAX requests. -.. code-block:: python - :linenos: +For example, include the CSRF token as a hidden field: - token = request.session.get_csrf_token() - if token != request.POST['csrf_token']: - raise ValueError('CSRF token did not match') +.. code-block:: html + + <form method="post" action="/myview"> + <input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}"> + <input type="submit" value="Delete Everything"> + </form> + +Or include it as a header in a jQuery AJAX request: + +.. code-block:: javascript + + var csrfToken = ${request.session.get_csrf_token()}; + $.ajax({ + type: "POST", + url: "/myview", + headers: { 'X-CSRF-Token': csrfToken } + }).done(function() { + alert("Deleted"); + }); + +The handler for the URL that receives the request should then require that the +correct CSRF token is supplied. + +.. index:: + single: session.new_csrf_token Using the ``session.new_csrf_token`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To explicitly add a new CSRF token to the session, use the -``session.new_csrf_token()`` method. This differs only from -``session.get_csrf_token()`` inasmuch as it clears any existing CSRF token, -creates a new CSRF token, sets the token into the session, and returns the -token. +To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` +method. This differs only from ``session.get_csrf_token()`` inasmuch as it +clears any existing CSRF token, creates a new CSRF token, sets the token into +the session, and returns the token. .. code-block:: python token = request.session.new_csrf_token() +Checking CSRF Tokens Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In request handling code, you can check the presence and validity of a CSRF +token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it +will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, +you can specify ``raises=False`` to have the check return ``False`` instead of +raising an exception. + +By default, it checks for a POST parameter named ``csrf_token`` or a header +named ``X-CSRF-Token``. + +.. code-block:: python + + from pyramid.session import check_csrf_token + + def myview(request): + # Require CSRF Token + check_csrf_token(request) + + # ... + +.. _auto_csrf_checking: + +Checking CSRF Tokens Automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +:app:`Pyramid` supports automatically checking CSRF tokens on requests with an +unsafe method as defined by RFC2616. Any other request may be checked manually. +This feature can be turned on globally for an application using the +``pyramid.require_default_csrf`` setting. + +If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or +``True`` then the default CSRF token parameter will be ``csrf_token``. If a +different token is desired, it may be passed as the value. Finally, a +:term:`falsey string` or ``False`` will turn off automatic CSRF checking +globally on every request. + +No matter what, CSRF checking may be explicitly enabled or disabled on a +per-view basis using the ``require_csrf`` view option. This option is of the +same format as the ``pyramid.require_default_csrf`` setting, accepting strings +or boolean values. + +If ``require_csrf`` is ``True`` but does not explicitly define a token to +check, then the token name is pulled from whatever was set in the +``pyramid.require_default_csrf`` setting. Finally, if that setting does not +explicitly define a token, then ``csrf_token`` is the token required. This token +name will be required in ``request.POST`` which is the submitted form body. + +It is always possible to pass the token in the ``X-CSRF-Token`` header as well. +There is currently no way to define an alternate name for this header without +performing CSRF checking manually. + +In addition to token based CSRF checks, the automatic CSRF checking will also +check the referrer of the request to ensure that it matches one of the trusted +origins. By default the only trusted origin is the current host, however +additional origins may be configured by setting +``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they +are non standard). If a host in the list of domains starts with a ``.`` then +that will allow all subdomains as well as the domain without the ``.``. + +If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception +will be raised. This exception may be caught and handled by an +:term:`exception view` but, by default, will result in a ``400 Bad Request`` +response being sent to the client. + +Checking CSRF Tokens with a View Predicate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead + to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + +A convenient way to require a valid CSRF token for a particular view is to +include ``check_csrf=True`` as a view predicate. See +:meth:`pyramid.config.Configurator.add_view`. + +.. code-block:: python + + @view_config(request_method='POST', check_csrf=True, ...) + def myview(request): + ... +.. note:: + A mismatch of a CSRF token is treated like any other predicate miss, and the + predicate system, when it doesn't find a view, raises ``HTTPNotFound`` + instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different + from calling :func:`pyramid.session.check_csrf_token`. diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index e2c43b17e..3e168eaea 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -6,58 +6,62 @@ Startup When you cause a :app:`Pyramid` application to start up in a console window, you'll see something much like this show up on the console: -.. code-block:: text +.. code-block:: bash - $ paster serve myproject/MyProject.ini - Starting server in PID 16601. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 + $ $VENV/bin/pserve development.ini + Starting server in PID 16305. + serving on http://127.0.0.1:6543 -This chapter explains what happens between the time you press the "Return" -key on your keyboard after typing ``paster serve myproject/MyProject.ini`` -and the time the line ``serving on 0.0.0.0:6543 ...`` is output to your -console. +This chapter explains what happens between the time you press the "Return" key +on your keyboard after typing ``pserve development.ini`` and the time the line +``serving on http://127.0.0.1:6543`` is output to your console. .. index:: single: startup process + pair: settings; .ini The Startup Process ------------------- The easiest and best-documented way to start and serve a :app:`Pyramid` -application is to use the ``paster serve`` command against a -:term:`PasteDeploy` ``.ini`` file. This uses the ``.ini`` file to infer -settings and starts a server listening on a port. For the purposes of this -discussion, we'll assume that you are using this command to run your -:app:`Pyramid` application. +application is to use the ``pserve`` command against a :term:`PasteDeploy` +``.ini`` file. This uses the ``.ini`` file to infer settings and starts a +server listening on a port. For the purposes of this discussion, we'll assume +that you are using this command to run your :app:`Pyramid` application. Here's a high-level time-ordered overview of what happens when you press -``return`` after running ``paster serve development.ini``. +``return`` after running ``pserve development.ini``. -#. The :term:`PasteDeploy` ``paster`` command is invoked under your shell - with the arguments ``serve`` and ``development.ini``. As a result, the - :term:`PasteDeploy` framework recognizes that it is meant to begin to run - and serve an application using the information contained within the - ``development.ini`` file. +#. The ``pserve`` command is invoked under your shell with the argument + ``development.ini``. As a result, Pyramid recognizes that it is meant to + begin to run and serve an application using the information contained + within the ``development.ini`` file. -#. The PasteDeploy framework finds a section named either ``[app:main]``, +#. The framework finds a section named either ``[app:main]``, ``[pipeline:main]``, or ``[composite:main]`` in the ``.ini`` file. This - section represents the configuration of a :term:`WSGI` application that - will be served. If you're using a simple application (e.g. - ``[app:main]``), the application :term:`entry point` or :term:`dotted - Python name` will be named on the ``use=`` line within the section's - configuration. If, instead of a simple application, you're using a WSGI - :term:`pipeline` (e.g. a ``[pipeline:main]`` section), the application - named on the "last" element will refer to your :app:`Pyramid` application. - If instead of a simple application or a pipeline, you're using a Paste - "composite" (e.g. ``[composite:main]``), refer to the documentation for - that particular composite to understand how to make it refer to your - :app:`Pyramid` application. - -#. The application's *constructor* (named by the entry point reference or - dotted Python name on the ``use=`` line of the section representing your - :app:`Pyramid` application) is passed the key/value parameters mentioned - within the section in which it's defined. The constructor is meant to - return a :term:`router` instance, which is a :term:`WSGI` application. + section represents the configuration of a :term:`WSGI` application that will + be served. If you're using a simple application (e.g., ``[app:main]``), the + application's ``paste.app_factory`` :term:`entry point` will be named on the + ``use=`` line within the section's configuration. If instead of a simple + application, you're using a WSGI :term:`pipeline` (e.g., a + ``[pipeline:main]`` section), the application named on the "last" element + will refer to your :app:`Pyramid` application. If instead of a simple + application or a pipeline, you're using a "composite" (e.g., + ``[composite:main]``), refer to the documentation for that particular + composite to understand how to make it refer to your :app:`Pyramid` + application. In most cases, a Pyramid application built from a scaffold + will have a single ``[app:main]`` section in it, and this will be the + application served. + +#. The framework finds all :mod:`logging` related configuration in the ``.ini`` + file and uses it to configure the Python standard library logging system for + this application. See :ref:`logging_config` for more information. + +#. The application's *constructor* named by the entry point referenced on the + ``use=`` line of the section representing your :app:`Pyramid` application is + passed the key/value parameters mentioned within the section in which it's + defined. The constructor is meant to return a :term:`router` instance, + which is a :term:`WSGI` application. For :app:`Pyramid` applications, the constructor will be a function named ``main`` in the ``__init__.py`` file within the :term:`package` in which @@ -71,13 +75,13 @@ Here's a high-level time-ordered overview of what happens when you press Note that the constructor function accepts a ``global_config`` argument, which is a dictionary of key/value pairs mentioned in the ``[DEFAULT]`` - section of an ``.ini`` file. It also accepts a ``**settings`` argument, - which collects another set of arbitrary key/value pairs. The arbitrary - key/value pairs received by this function in ``**settings`` will be - composed of all the key/value pairs that are present in the - ``[app:MyProject]`` section (except for the ``use=`` setting) when this - function is called by the :term:`PasteDeploy` framework when you run - ``paster serve``. + section of an ``.ini`` file (if :ref:`[DEFAULT] + <defaults_section_of_pastedeploy_file>` is present). It also accepts a + ``**settings`` argument, which collects another set of arbitrary key/value + pairs. The arbitrary key/value pairs received by this function in + ``**settings`` will be composed of all the key/value pairs that are present + in the ``[app:main]`` section (except for the ``use=`` setting) when this + function is called when you run ``pserve``. Our generated ``development.ini`` file looks like so: @@ -87,54 +91,59 @@ Here's a high-level time-ordered overview of what happens when you press In this case, the ``myproject.__init__:main`` function referred to by the entry point URI ``egg:MyProject`` (see :ref:`MyProject_ini` for more - information about entry point URIs, and how they relate to callables), - will receive the key/value pairs ``{'reload_templates':'true', - 'debug_authorization':'false', 'debug_notfound':'false', - 'debug_routematch':'false', 'debug_templates':'true', - 'default_locale_name':'en'}``. + information about entry point URIs, and how they relate to callables) will + receive the key/value pairs ``{pyramid.reload_templates = true, + pyramid.debug_authorization = false, pyramid.debug_notfound = false, + pyramid.debug_routematch = false, pyramid.default_locale_name = en, and + pyramid.includes = pyramid_debugtoolbar}``. See :ref:`environment_chapter` + for the meanings of these keys. #. The ``main`` function first constructs a - :class:`~pyramid.config.Configurator` instance, passing a root resource - factory (constructor) to it as its ``root_factory`` argument, and - ``settings`` dictionary captured via the ``**settings`` kwarg as its - ``settings`` argument. - - The root resource factory is invoked on every request to retrieve the - application's root resource. It is not called during startup, only when a - request is handled. - - The ``settings`` dictionary contains all the options in the - ``[app:MyProject]`` section of our .ini file except the ``use`` option - (which is internal to Paste) such as ``reload_templates``, - ``debug_authorization``, etc. - -#. The ``main`` function then calls various methods on the an instance of the - class :class:`~pyramid.config.Configurator` method. The intent of - calling these methods is to populate an :term:`application registry`, - which represents the :app:`Pyramid` configuration related to the + :class:`~pyramid.config.Configurator` instance, passing the ``settings`` + dictionary captured via the ``**settings`` kwarg as its ``settings`` + argument. + + The ``settings`` dictionary contains all the options in the ``[app:main]`` + section of our .ini file except the ``use`` option (which is internal to + PasteDeploy) such as ``pyramid.reload_templates``, + ``pyramid.debug_authorization``, etc. + +#. The ``main`` function then calls various methods on the instance of the + class :class:`~pyramid.config.Configurator` created in the previous step. + The intent of calling these methods is to populate an :term:`application + registry`, which represents the :app:`Pyramid` configuration related to the application. -#. The :meth:`~pyramid.config.Configurator.make_wsgi_app` method is called. - The result is a :term:`router` instance. The router is associated with - the :term:`application registry` implied by the configurator previously +#. The :meth:`~pyramid.config.Configurator.make_wsgi_app` method is called. The + result is a :term:`router` instance. The router is associated with the + :term:`application registry` implied by the configurator previously populated by other methods run against the Configurator. The router is a WSGI application. -#. A :class:`~pyramid.events.ApplicationCreated` event is emitted (see +#. An :class:`~pyramid.events.ApplicationCreated` event is emitted (see :ref:`events_chapter` for more information about events). #. Assuming there were no errors, the ``main`` function in ``myproject`` - returns the router instance created by ``make_wsgi_app`` back to - PasteDeploy. As far as PasteDeploy is concerned, it is "just another WSGI - application". - -#. PasteDeploy starts the WSGI *server* defined within the ``[server:main]`` - section. In our case, this is the ``Paste#http`` server (``use = - egg:Paste#http``), and it will listen on all interfaces (``host = - 0.0.0.0``), on port number 6543 (``port = 6543``). The server code itself - is what prints ``serving on 0.0.0.0:6543 view at http://127.0.0.1:6543``. - The server serves the application, and the application is running, waiting - to receive requests. + returns the router instance created by + :meth:`pyramid.config.Configurator.make_wsgi_app` back to ``pserve``. As + far as ``pserve`` is concerned, it is "just another WSGI application". + +#. ``pserve`` starts the WSGI *server* defined within the ``[server:main]`` + section. In our case, this is the Waitress server (``use = + egg:waitress#main``), and it will listen on all interfaces (``host = + 127.0.0.1``), on port number 6543 (``port = 6543``). The server code itself + is what prints ``serving on http://127.0.0.1:6543``. The server serves the + application, and the application is running, waiting to receive requests. + +.. seealso:: + Logging configuration is described in the :ref:`logging_chapter` chapter. + There, in :ref:`request_logging_with_pastes_translogger`, you will also find + an example of how to configure :term:`middleware` to add pre-packaged + functionality to your application. + +.. index:: + pair: settings; deployment + single: custom settings .. _deployment_settings: @@ -143,8 +152,7 @@ Deployment Settings Note that an augmented version of the values passed as ``**settings`` to the :class:`~pyramid.config.Configurator` constructor will be available in -:app:`Pyramid` :term:`view callable` code as ``request.registry.settings``. -You can create objects you wish to access later from view code, and put them -into the dictionary you pass to the configurator as ``settings``. They will -then be present in the ``request.registry.settings`` dictionary at -application runtime. +:app:`Pyramid` :term:`view callable` code as ``request.registry.settings``. You +can create objects you wish to access later from view code, and put them into +the dictionary you pass to the configurator as ``settings``. They will then be +present in the ``request.registry.settings`` dictionary at application runtime. diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst new file mode 100644 index 000000000..7c847de50 --- /dev/null +++ b/docs/narr/subrequest.rst @@ -0,0 +1,331 @@ +.. index:: + single: subrequest + +.. _subrequest_chapter: + +Invoking a Subrequest +===================== + +.. versionadded:: 1.4 + +:app:`Pyramid` allows you to invoke a subrequest at any point during the +processing of a request. Invoking a subrequest allows you to obtain a +:term:`response` object from a view callable within your :app:`Pyramid` +application while you're executing a different view callable within the same +application. + +Here's an example application which uses a subrequest: + +.. code-block:: python + :linenos: + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.request import Request + + def view_one(request): + subreq = Request.blank('/view_two') + response = request.invoke_subrequest(subreq) + return response + + def view_two(request): + request.response.body = 'This came from view_two' + return request.response + + if __name__ == '__main__': + config = Configurator() + config.add_route('one', '/view_one') + config.add_route('two', '/view_two') + config.add_view(view_one, route_name='one') + config.add_view(view_two, route_name='two') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() + +When ``/view_one`` is visted in a browser, the text printed in the browser pane +will be ``This came from view_two``. The ``view_one`` view used the +:meth:`pyramid.request.Request.invoke_subrequest` API to obtain a response from +another view (``view_two``) within the same application when it executed. It +did so by constructing a new request that had a URL that it knew would match +the ``view_two`` view registration, and passed that new request along to +:meth:`pyramid.request.Request.invoke_subrequest`. The ``view_two`` view +callable was invoked, and it returned a response. The ``view_one`` view +callable then simply returned the response it obtained from the ``view_two`` +view callable. + +Note that it doesn't matter if the view callable invoked via a subrequest +actually returns a *literal* Response object. Any view callable that uses a +renderer or which returns an object that can be interpreted by a response +adapter when found and invoked via +:meth:`pyramid.request.Request.invoke_subrequest` will return a Response +object: + +.. code-block:: python + :linenos: + :emphasize-lines: 11 + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.request import Request + + def view_one(request): + subreq = Request.blank('/view_two') + response = request.invoke_subrequest(subreq) + return response + + def view_two(request): + return 'This came from view_two' + + if __name__ == '__main__': + config = Configurator() + config.add_route('one', '/view_one') + config.add_route('two', '/view_two') + config.add_view(view_one, route_name='one') + config.add_view(view_two, route_name='two', renderer='string') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() + +Even though the ``view_two`` view callable returned a string, it was invoked in +such a way that the ``string`` renderer associated with the view registration +that was found turned it into a "real" response object for consumption by +``view_one``. + +Being able to unconditionally obtain a response object by invoking a view +callable indirectly is the main advantage to using +:meth:`pyramid.request.Request.invoke_subrequest` instead of simply importing a +view callable and executing it directly. Note that there's not much advantage +to invoking a view using a subrequest if you *can* invoke a view callable +directly. Subrequests are slower and are less convenient if you actually do +want just the literal information returned by a function that happens to be a +view callable. + +Note that, by default, if a view callable invoked by a subrequest raises an +exception, the exception will be raised to the caller of +:meth:`~pyramid.request.Request.invoke_subrequest` even if you have a +:term:`exception view` configured: + +.. code-block:: python + :linenos: + :emphasize-lines: 11-16 + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.request import Request + + def view_one(request): + subreq = Request.blank('/view_two') + response = request.invoke_subrequest(subreq) + return response + + def view_two(request): + raise ValueError('foo') + + def excview(request): + request.response.body = b'An exception was raised' + request.response.status_int = 500 + return request.response + + if __name__ == '__main__': + config = Configurator() + config.add_route('one', '/view_one') + config.add_route('two', '/view_two') + config.add_view(view_one, route_name='one') + config.add_view(view_two, route_name='two', renderer='string') + config.add_view(excview, context=Exception) + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() + +When we run the above code and visit ``/view_one`` in a browser, the +``excview`` :term:`exception view` will *not* be executed. Instead, the call +to :meth:`~pyramid.request.Request.invoke_subrequest` will cause a +:exc:`ValueError` exception to be raised and a response will never be +generated. We can change this behavior; how to do so is described below in our +discussion of the ``use_tweens`` argument. + +.. index:: + pair: subrequest; use_tweens + +Subrequests with Tweens +----------------------- + +The :meth:`pyramid.request.Request.invoke_subrequest` API accepts two +arguments: a required positional argument ``request``, and an optional keyword +argument ``use_tweens`` which defaults to ``False``. + +The ``request`` object passed to the API must be an object that implements the +Pyramid request interface (such as a :class:`pyramid.request.Request` +instance). If ``use_tweens`` is ``True``, the request will be sent to the +:term:`tween` in the tween stack closest to the request ingress. If +``use_tweens`` is ``False``, the request will be sent to the main router +handler, and no tweens will be invoked. + +In the example above, the call to +:meth:`~pyramid.request.Request.invoke_subrequest` will always raise an +exception. This is because it's using the default value for ``use_tweens``, +which is ``False``. Alternatively, you can pass ``use_tweens=True`` to ensure +that it will convert an exception to a Response if an :term:`exception view` is +configured, instead of raising the exception. This is because exception views +are called by the exception view :term:`tween` as described in +:ref:`exception_views` when any view raises an exception. + +We can cause the subrequest to be run through the tween stack by passing +``use_tweens=True`` to the call to +:meth:`~pyramid.request.Request.invoke_subrequest`, like this: + +.. code-block:: python + :linenos: + :emphasize-lines: 7 + + from wsgiref.simple_server import make_server + from pyramid.config import Configurator + from pyramid.request import Request + + def view_one(request): + subreq = Request.blank('/view_two') + response = request.invoke_subrequest(subreq, use_tweens=True) + return response + + def view_two(request): + raise ValueError('foo') + + def excview(request): + request.response.body = b'An exception was raised' + request.response.status_int = 500 + return request.response + + if __name__ == '__main__': + config = Configurator() + config.add_route('one', '/view_one') + config.add_route('two', '/view_two') + config.add_view(view_one, route_name='one') + config.add_view(view_two, route_name='two', renderer='string') + config.add_view(excview, context=Exception) + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() + +In the above case, the call to ``request.invoke_subrequest(subreq)`` will not +raise an exception. Instead, it will retrieve a "500" response from the +attempted invocation of ``view_two``, because the tween which invokes an +exception view to generate a response is run, and therefore ``excview`` is +executed. + +This is one of the major differences between specifying the ``use_tweens=True`` +and ``use_tweens=False`` arguments to +:meth:`~pyramid.request.Request.invoke_subrequest`. ``use_tweens=True`` may +also imply invoking a transaction commit or abort for the logic executed in the +subrequest if you've got ``pyramid_tm`` in the tween list, injecting debug HTML +if you've got ``pyramid_debugtoolbar`` in the tween list, and other +tween-related side effects as defined by your particular tween list. + +The :meth:`~pyramid.request.Request.invoke_subrequest` function also +unconditionally does the following: + +- It manages the threadlocal stack so that + :func:`~pyramid.threadlocal.get_current_request` and + :func:`~pyramid.threadlocal.get_current_registry` work during a request (they + will return the subrequest instead of the original request). + +- It adds a ``registry`` attribute and an ``invoke_subrequest`` attribute (a + callable) to the request object to which it is handed. + +- It sets request extensions (such as those added via + :meth:`~pyramid.config.Configurator.add_request_method` or + :meth:`~pyramid.config.Configurator.set_request_property`) on the subrequest + object passed as ``request``. + +- It causes a :class:`~pyramid.events.NewRequest` event to be sent at the + beginning of request processing. + +- It causes a :class:`~pyramid.events.ContextFound` event to be sent when a + context resource is found. + +- It ensures that the user implied by the request passed in has the necessary + authorization to invoke the view callable before calling it. + +- It calls any :term:`response callback` functions defined within the + subrequest's lifetime if a response is obtained from the Pyramid application. + +- It causes a :class:`~pyramid.events.NewResponse` event to be sent if a + response is obtained. + +- It calls any :term:`finished callback` functions defined within the + subrequest's lifetime. + +The invocation of a subrequest has more or less exactly the same effect as the +invocation of a request received by the :app:`Pyramid` router from a web client +when ``use_tweens=True``. When ``use_tweens=False``, the tweens are skipped +but all the other steps take place. + +It's a poor idea to use the original ``request`` object as an argument to +:meth:`~pyramid.request.Request.invoke_subrequest`. You should construct a new +request instead as demonstrated in the above example, using +:meth:`pyramid.request.Request.blank`. Once you've constructed a request +object, you'll need to massage it to match the view callable that you'd like to +be executed during the subrequest. This can be done by adjusting the +subrequest's URL, its headers, its request method, and other attributes. The +documentation for :class:`pyramid.request.Request` exposes the methods you +should call and attributes you should set on the request that you create, then +massage it into something that will actually match the view you'd like to call +via a subrequest. + +We've demonstrated use of a subrequest from within a view callable, but you can +use the :meth:`~pyramid.request.Request.invoke_subrequest` API from within a +tween or an event handler as well. Even though you can do it, it's usually a +poor idea to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from +within a tween, because tweens already, by definition, have access to a +function that will cause a subrequest (they are passed a ``handle`` function). +It's fine to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from +within an event handler, however. + + +.. index:: + pair: subrequest; exception view + +Invoking an Exception View +-------------------------- + +.. versionadded:: 1.7 + +:app:`Pyramid` apps may define :term:`exception views <exception view>` which +can handle any raised exceptions that escape from your code while processing +a request. By default an unhandled exception will be caught by the ``EXCVIEW`` +:term:`tween`, which will then lookup an exception view that can handle the +exception type, generating an appropriate error response. + +In :app:`Pyramid` 1.7 the :meth:`pyramid.request.Request.invoke_exception_view` +was introduced, allowing a user to invoke an exception view while manually +handling an exception. This can be useful in a few different circumstances: + +- Manually handling an exception losing the current call stack or flow. + +- Handling exceptions outside of the context of the ``EXCVIEW`` tween. The + tween only covers certain parts of the request processing pipeline (See + :ref:`router_chapter`). There are also some corner cases where an exception + can be raised that will still bubble up to middleware, and possibly to the + web server in which case a generic ``500 Internal Server Error`` will be + returned to the client. + +Below is an example usage of +:meth:`pyramid.request.Request.invoke_exception_view`: + +.. code-block:: python + :linenos: + + def foo(request): + try: + some_func_that_errors() + return response + except Exception: + response = request.invoke_exception_view() + if response is not None: + return response + else: + # there is no exception view for this exception, simply + # re-raise and let someone else handle it + raise + +Please note that in most cases you do not need to write code like this, and you +may rely on the ``EXCVIEW`` tween to handle this for you. diff --git a/docs/narr/tb_introspector.png b/docs/narr/tb_introspector.png Binary files differnew file mode 100644 index 000000000..b00d36067 --- /dev/null +++ b/docs/narr/tb_introspector.png diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 150b173e3..9e3a31845 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -3,19 +3,14 @@ Templates ========= -A :term:`template` is a file on disk which can be used to render -dynamic data provided by a :term:`view`. :app:`Pyramid` offers a -number of ways to perform templating tasks out of the box, and -provides add-on templating support through a set of bindings packages. +A :term:`template` is a file on disk which can be used to render dynamic data +provided by a :term:`view`. :app:`Pyramid` offers a number of ways to perform +templating tasks out of the box, and provides add-on templating support through +a set of bindings packages. -Out of the box, :app:`Pyramid` provides templating via the :term:`Chameleon` -and :term:`Mako` templating libraries. :term:`Chameleon` provides support for -two different types of templates: :term:`ZPT` templates, and text templates. - -Before discussing how built-in templates are used in -detail, we'll discuss two ways to render templates within -:app:`Pyramid` in general: directly, and via renderer -configuration. +Before discussing how built-in templates are used in detail, we'll discuss two +ways to render templates within :app:`Pyramid` in general: directly and via +renderer configuration. .. index:: single: templates used directly @@ -25,16 +20,15 @@ configuration. Using Templates Directly ------------------------ -The most straightforward way to use a template within -:app:`Pyramid` is to cause it to be rendered directly within a -:term:`view callable`. You may use whatever API is supplied by a -given templating engine to do so. +The most straightforward way to use a template within :app:`Pyramid` is to +cause it to be rendered directly within a :term:`view callable`. You may use +whatever API is supplied by a given templating engine to do so. -:app:`Pyramid` provides various APIs that allow you to render templates -directly from within a view callable. For example, if there is a -:term:`Chameleon` ZPT template named ``foo.pt`` in a directory named -``templates`` in your application, you can render the template from -within the body of a view callable like so: +:app:`Pyramid` provides various APIs that allow you to render templates directly +from within a view callable. For example, if there is a :term:`Chameleon` ZPT +template named ``foo.pt`` in a directory named ``templates`` in your +application, you can render the template from within the body of a view +callable like so: .. code-block:: python :linenos: @@ -42,51 +36,26 @@ within the body of a view callable like so: from pyramid.renderers import render_to_response def sample_view(request): - return render_to_response('templates/foo.pt', - {'foo':1, 'bar':2}, + return render_to_response('templates/foo.pt', + {'foo':1, 'bar':2}, request=request) -.. warning:: Earlier iterations of this documentation - (pre-version-1.3) encouraged the application developer to use - ZPT-specific APIs such as - :func:`pyramid.chameleon_zpt.render_template_to_response` and - :func:`pyramid.chameleon_zpt.render_template` to render templates - directly. This style of rendering still works, but at least for - purposes of this documentation, those functions are deprecated. - Application developers are encouraged instead to use the functions - available in the :mod:`pyramid.renderers` module to perform - rendering tasks. This set of functions works to render templates - for all renderer extensions registered with :app:`Pyramid`. - The ``sample_view`` :term:`view callable` function above returns a -:term:`response` object which contains the body of the -``templates/foo.pt`` template. In this case, the ``templates`` -directory should live in the same directory as the module containing -the ``sample_view`` function. The template author will have the names -``foo`` and ``bar`` available as top-level names for replacement or -comparison purposes. +:term:`response` object which contains the body of the ``templates/foo.pt`` +template. In this case, the ``templates`` directory should live in the same +directory as the module containing the ``sample_view`` function. The template +author will have the names ``foo`` and ``bar`` available as top-level names for +replacement or comparison purposes. In the example above, the path ``templates/foo.pt`` is relative to the -directory containing the file which defines the view configuration. -In this case, this is the directory containing the file that -defines the ``sample_view`` function. Although a renderer path is -usually just a simple relative pathname, a path named as a renderer -can be absolute, starting with a slash on UNIX or a drive letter -prefix on Windows. - -.. warning:: - - Only :term:`Chameleon` templates support defining a renderer for a - template relative to the location of the module where the view - callable is defined. Mako templates, and other templating system - bindings work differently. In particular, Mako templates use a - "lookup path" as defined by the ``mako.directories`` configuration - file instead of treating relative paths as relative to the current - view module. See :ref:`mako_templates`. - -The path can alternately be a :term:`asset specification` in the form -``some.dotted.package_name:relative/path``. This makes it possible to -address template assets which live in another package. For example: +directory containing the file which defines the view configuration. In this +case, this is the directory containing the file that defines the +``sample_view`` function. Although a renderer path is usually just a simple +relative pathname, a path named as a renderer can be absolute, starting with a +slash on UNIX or a drive letter prefix on Windows. The path can alternatively +be an :term:`asset specification` in the form +``some.dotted.package_name:relative/path``. This makes it possible to address +template assets which live in another package. For example: .. code-block:: python :linenos: @@ -98,45 +67,36 @@ address template assets which live in another package. For example: {'foo':1, 'bar':2}, request=request) -An asset specification points at a file within a Python *package*. -In this case, it points at a file named ``foo.pt`` within the -``templates`` directory of the ``mypackage`` package. Using a -asset specification instead of a relative template name is usually -a good idea, because calls to ``render_to_response`` using asset -specifications will continue to work properly if you move the code -containing them around. - -.. note:: - - Mako templating system bindings also respect absolute asset - specifications as an argument to any of the ``render*`` commands. If a - template name defines a ``:`` (colon) character and is not an absolute - path, it is treated as an absolute asset specification. +An asset specification points at a file within a Python *package*. In this +case, it points at a file named ``foo.pt`` within the ``templates`` directory +of the ``mypackage`` package. Using an asset specification instead of a +relative template name is usually a good idea, because calls to +:func:`~pyramid.renderers.render_to_response` using asset specifications will +continue to work properly if you move the code containing them to another +location. In the examples above we pass in a keyword argument named ``request`` -representing the current :app:`Pyramid` request. Passing a request -keyword argument will cause the ``render_to_response`` function to -supply the renderer with more correct system values (see -:ref:`renderer_system_values`), because most of the information required -to compose proper system values is present in the request. If your -template relies on the name ``request`` or ``context``, or if you've -configured special :term:`renderer globals`, make sure to pass -``request`` as a keyword argument in every call to to a +representing the current :app:`Pyramid` request. Passing a request keyword +argument will cause the ``render_to_response`` function to supply the renderer +with more correct system values (see :ref:`renderer_system_values`), because +most of the information required to compose proper system values is present in +the request. If your template relies on the name ``request`` or ``context``, +or if you've configured special :term:`renderer globals`, make sure to pass +``request`` as a keyword argument in every call to a ``pyramid.renderers.render_*`` function. -Every view must return a :term:`response` object, except for views -which use a :term:`renderer` named via view configuration (which we'll -see shortly). The :func:`pyramid.renderers.render_to_response` -function is a shortcut function that actually returns a response -object. This allows the example view above to simply return the result -of its call to ``render_to_response()`` directly. +Every view must return a :term:`response` object, except for views which use a +:term:`renderer` named via view configuration (which we'll see shortly). The +:func:`pyramid.renderers.render_to_response` function is a shortcut function +that actually returns a response object. This allows the example view above to +simply return the result of its call to ``render_to_response()`` directly. Obviously not all APIs you might call to get response data will return a -response object. For example, you might render one or more templates to -a string that you want to use as response data. The -:func:`pyramid.renderers.render` API renders a template to a string. We -can manufacture a :term:`response` object directly, and use that string -as the body of the response: +response object. For example, you might render one or more templates to a +string that you want to use as response data. The +:func:`pyramid.renderers.render` API renders a template to a string. We can +manufacture a :term:`response` object directly, and use that string as the body +of the response: .. code-block:: python :linenos: @@ -145,24 +105,23 @@ as the body of the response: from pyramid.response import Response def sample_view(request): - result = render('mypackage:templates/foo.pt', - {'foo':1, 'bar':2}, + result = render('mypackage:templates/foo.pt', + {'foo':1, 'bar':2}, request=request) response = Response(result) return response Because :term:`view callable` functions are typically the only code in :app:`Pyramid` that need to know anything about templates, and because view -functions are very simple Python, you can use whatever templating system you're -most comfortable with within :app:`Pyramid`. Install the templating system, -import its API functions into your views module, use those APIs to generate a -string, then return that string as the body of a :app:`Pyramid` +functions are very simple Python, you can use whatever templating system with +which you're most comfortable within :app:`Pyramid`. Install the templating +system, import its API functions into your views module, use those APIs to +generate a string, then return that string as the body of a :app:`Pyramid` :term:`Response` object. -For example, here's an example of using "raw" `Mako -<http://www.makotemplates.org/>`_ from within a :app:`Pyramid` :term:`view`: +For example, here's an example of using "raw" Mako_ from within a +:app:`Pyramid` :term:`view`: -.. ignore-next-block .. code-block:: python :linenos: @@ -176,34 +135,32 @@ For example, here's an example of using "raw" `Mako return response You probably wouldn't use this particular snippet in a project, because it's -easier to use the Mako renderer bindings which already exist in -:app:`Pyramid`. But if your favorite templating system is not supported as a -renderer extension for :app:`Pyramid`, you can create your own simple -combination as shown above. +easier to use the supported :ref:`Mako bindings +<available_template_system_bindings>`. But if your favorite templating system +is not supported as a renderer extension for :app:`Pyramid`, you can create +your own simple combination as shown above. .. note:: If you use third-party templating languages without cooperating :app:`Pyramid` bindings directly within view callables, the - auto-template-reload strategy explained in - :ref:`reload_templates_section` will not be available, nor will the - template asset overriding capability explained in - :ref:`overriding_assets_section` be available, nor will it be - possible to use any template using that language as a - :term:`renderer`. However, it's reasonably easy to write custom - templating system binding packages for use under :app:`Pyramid` so - that templates written in the language can be used as renderers. - See :ref:`adding_and_overriding_renderers` for instructions on how - to create your own template renderer and - :ref:`available_template_system_bindings` for example packages. - -If you need more control over the status code and content-type, or -other response attributes from views that use direct templating, you -may set attributes on the response that influence these values. - -Here's an example of changing the content-type and status of the -response object returned by -:func:`~pyramid.renderers.render_to_response`: + auto-template-reload strategy explained in :ref:`reload_templates_section` + will not be available, nor will the template asset overriding capability + explained in :ref:`overriding_assets_section` be available, nor will it be + possible to use any template using that language as a :term:`renderer`. + However, it's reasonably easy to write custom templating system binding + packages for use under :app:`Pyramid` so that templates written in the + language can be used as renderers. See + :ref:`adding_and_overriding_renderers` for instructions on how to create + your own template renderer and :ref:`available_template_system_bindings` + for example packages. + +If you need more control over the status code and content-type, or other +response attributes from views that use direct templating, you may set +attributes on the response that influence these values. + +Here's an example of changing the content-type and status of the response +object returned by :func:`~pyramid.renderers.render_to_response`: .. code-block:: python :linenos: @@ -218,8 +175,8 @@ response object returned by response.status_int = 204 return response -Here's an example of manufacturing a response object using the result -of :func:`~pyramid.renderers.render` (a string): +Here's an example of manufacturing a response object using the result of +:func:`~pyramid.renderers.render` (a string): .. code-block:: python :linenos: @@ -229,7 +186,7 @@ of :func:`~pyramid.renderers.render` (a string): def sample_view(request): result = render('mypackage:templates/foo.pt', - {'foo':1, 'bar':2}, + {'foo':1, 'bar':2}, request=request) response = Response(result) response.content_type = 'text/plain' @@ -241,67 +198,86 @@ of :func:`~pyramid.renderers.render` (a string): single: renderer (template) +.. index:: + pair: renderer; system values + .. _renderer_system_values: System Values Used During Rendering ----------------------------------- -When a template is rendered using -:func:`~pyramid.renderers.render_to_response` or -:func:`~pyramid.renderers.render`, the renderer representing the -template will be provided with a number of *system* values. These -values are provided in a dictionary to the renderer and include: - -``context`` - The current :app:`Pyramid` context if ``request`` was provided as - a keyword argument, or ``None``. +When a template is rendered using :func:`~pyramid.renderers.render_to_response` +or :func:`~pyramid.renderers.render`, or a ``renderer=`` argument to view +configuration (see :ref:`templates_used_as_renderers`), the renderer +representing the template will be provided with a number of *system* values. +These values are provided to the template: ``request`` - The request provided as a keyword argument. + The value provided as the ``request`` keyword argument to + ``render_to_response`` or ``render`` *or* the request object passed to the + view when the ``renderer=`` argument to view configuration is being used to + render the template. + +``req`` + An alias for ``request``. + +``context`` + The current :app:`Pyramid` :term:`context` if ``request`` was provided as a + keyword argument to ``render_to_response`` or ``render``, or ``None`` if the + ``request`` keyword argument was not provided. This value will always be + provided if the template is rendered as the result of a ``renderer=`` + argument to the view configuration being used. ``renderer_name`` - The renderer name used to perform the rendering, - e.g. ``mypackage:templates/foo.pt``. + The renderer name used to perform the rendering, e.g., + ``mypackage:templates/foo.pt``. -``renderer_info`` +``renderer_info`` An object implementing the :class:`pyramid.interfaces.IRendererInfo` - interface. Basically, an object with the following attributes: - ``name``, ``package`` and ``type``. + interface. Basically, an object with the following attributes: ``name``, + ``package``, and ``type``. + +``view`` + The view callable object that was used to render this template. If the view + callable is a method of a class-based view, this will be an instance of the + class that the method was defined on. If the view callable is a function or + instance, it will be that function or instance. Note that this value will + only be automatically present when a template is rendered as a result of a + ``renderer=`` argument; it will be ``None`` when the ``render_to_response`` + or ``render`` APIs are used. -You can define more values which will be passed to every template -executed as a result of rendering by defining :term:`renderer -globals`. +You can define more values which will be passed to every template executed as a +result of rendering by defining :term:`renderer globals`. What any particular renderer does with these system values is up to the -renderer itself, but most template renderers, including Chameleon and -Mako renderers, make these names available as top-level template -variables. +renderer itself, but most template renderers make these names available as +top-level template variables. + +.. index:: + pair: renderer; templates .. _templates_used_as_renderers: Templates Used as Renderers via Configuration --------------------------------------------- -An alternative to using :func:`~pyramid.renderers.render_to_response` -to render templates manually in your view callable code, is -to specify the template as a :term:`renderer` in your -*view configuration*. This can be done with any of the +An alternative to using :func:`~pyramid.renderers.render_to_response` to render +templates manually in your view callable code is to specify the template as a +:term:`renderer` in your *view configuration*. This can be done with any of the templating languages supported by :app:`Pyramid`. -To use a renderer via view configuration, specify a template -:term:`asset specification` as the ``renderer`` argument, or -attribute to the :term:`view configuration` of a :term:`view -callable`. Then return a *dictionary* from that view callable. The -dictionary items returned by the view callable will be made available -to the renderer template as top-level names. +To use a renderer via view configuration, specify a template :term:`asset +specification` as the ``renderer`` argument, or attribute to the :term:`view +configuration` of a :term:`view callable`. Then return a *dictionary* from +that view callable. The dictionary items returned by the view callable will be +made available to the renderer template as top-level names. -The association of a template as a renderer for a :term:`view -configuration` makes it possible to replace code within a :term:`view -callable` that handles the rendering of a template. +The association of a template as a renderer for a :term:`view configuration` +makes it possible to replace code within a :term:`view callable` that handles +the rendering of a template. -Here's an example of using a :class:`~pyramid.view.view_config` -decorator to specify a :term:`view configuration` that names a -template renderer: +Here's an example of using a :class:`~pyramid.view.view_config` decorator to +specify a :term:`view configuration` that names a template renderer: .. code-block:: python :linenos: @@ -312,11 +288,12 @@ template renderer: def my_view(request): return {'foo':1, 'bar':2} -.. note:: You do not need to supply the ``request`` value as a key - in the dictionary result returned from a renderer-configured view - callable. :app:`Pyramid` automatically supplies this value for - you so that the "most correct" system values are provided to - the renderer. +.. note:: + + You do not need to supply the ``request`` value as a key in the dictionary + result returned from a renderer-configured view callable. :app:`Pyramid` + automatically supplies this value for you, so that the "most correct" system + values are provided to the renderer. .. warning:: @@ -324,328 +301,63 @@ template renderer: shown above is the template *path*. In the example above, the path ``templates/foo.pt`` is *relative*. Relative to what, you ask? Because we're using a Chameleon renderer, it means "relative to the directory in - which the file which defines the view configuration lives". In this case, + which the file that defines the view configuration lives". In this case, this is the directory containing the file that defines the ``my_view`` - function. View-configuration-relative asset specifications work only - in Chameleon, not in Mako templates. + function. + +Similar renderer configuration can be done imperatively. See +:ref:`views_which_use_a_renderer`. -Similar renderer configuration can be done imperatively and via -:term:`ZCML`. See :ref:`views_which_use_a_renderer`. See also -:ref:`built_in_renderers`. +.. seealso:: + + See also :ref:`built_in_renderers`. Although a renderer path is usually just a simple relative pathname, a path named as a renderer can be absolute, starting with a slash on UNIX or a drive -letter prefix on Windows. The path can alternately be an :term:`asset +letter prefix on Windows. The path can alternatively be an :term:`asset specification` in the form ``some.dotted.package_name:relative/path``, making it possible to address template assets which live in another package. Not just any template from any arbitrary templating system may be used as a renderer. Bindings must exist specifically for :app:`Pyramid` to use a -templating language template as a renderer. Currently, :app:`Pyramid` has -built-in support for two Chameleon templating languages: ZPT and text, and -the Mako templating system. See :ref:`built_in_renderers` for a discussion -of their details. :app:`Pyramid` also supports the use of :term:`Jinja2` -templates as renderers. See :ref:`available_template_system_bindings`. - -.. sidebar:: Why Use A Renderer via View Configuration - - Using a renderer in view configuration is usually a better way to - render templates than using any rendering API directly from within a - :term:`view callable` because it makes the view callable more - unit-testable. Views which use templating or rendering APIs directly - must return a :term:`Response` object. Making testing assertions - about response objects is typically an indirect process, because it - means that your test code often needs to somehow parse information - out of the response body (often HTML). View callables configured - with renderers externally via view configuration typically return a - dictionary, as above. Making assertions about results returned in a - dictionary is almost always more direct and straightforward than - needing to parse HTML. Specifying a renderer from within - :term:`ZCML` (as opposed to imperatively or via a ``view_config`` - decorator, or using a template directly from within a view callable) - also makes it possible for someone to modify the template used to - render a view without needing to fork your code to do so. See - :ref:`extending_chapter` for more information. +templating language template as a renderer. + +.. sidebar:: Why Use a Renderer via View Configuration + + Using a renderer in view configuration is usually a better way to render + templates than using any rendering API directly from within a :term:`view + callable` because it makes the view callable more unit-testable. Views + which use templating or rendering APIs directly must return a + :term:`Response` object. Making testing assertions about response objects + is typically an indirect process, because it means that your test code often + needs to somehow parse information out of the response body (often HTML). + View callables configured with renderers externally via view configuration + typically return a dictionary, as above. Making assertions about results + returned in a dictionary is almost always more direct and straightforward + than needing to parse HTML. By default, views rendered via a template renderer return a :term:`Response` object which has a *status code* of ``200 OK``, and a *content-type* of ``text/html``. To vary attributes of the response of a view that uses a -renderer, such as the content-type, headers, or status attributes, you must -use the API of the :class:`pyramid.response.Response` object exposed as +renderer, such as the content-type, headers, or status attributes, you must use +the API of the :class:`pyramid.response.Response` object exposed as ``request.response`` within the view before returning the dictionary. See :ref:`request_response_attr` for more information. -The same set of system values are provided to templates rendered via a -renderer view configuration as those provided to templates rendered -imperatively. See :ref:`renderer_system_values`. - - -.. index:: - single: Chameleon ZPT templates - single: ZPT templates (Chameleon) - -.. _chameleon_zpt_templates: - -:term:`Chameleon` ZPT Templates -------------------------------- - -Like :term:`Zope`, :app:`Pyramid` uses :term:`ZPT` (Zope Page -Templates) as its default templating language. However, -:app:`Pyramid` uses a different implementation of the :term:`ZPT` -specification than Zope does: the :term:`Chameleon` templating -engine. The Chameleon engine complies largely with the `Zope Page -Template <http://wiki.zope.org/ZPT/FrontPage>`_ template -specification. However, it is significantly faster. - -The language definition documentation for Chameleon ZPT-style -templates is available from `the Chameleon website -<http://chameleon.repoze.org/>`_. - -.. warning:: - - :term:`Chameleon` only works on :term:`CPython` platforms and - :term:`Google App Engine`. On :term:`Jython` and other non-CPython - platforms, you should use Mako (see :ref:`mako_templates`) or - ``pyramid_jinja2`` instead. See - :ref:`available_template_system_bindings`. - -Given a :term:`Chameleon` ZPT template named ``foo.pt`` in a directory -in your application named ``templates``, you can render the template as -a :term:`renderer` like so: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - - @view_config(renderer='templates/foo.pt') - def my_view(request): - return {'foo':1, 'bar':2} - -See also :ref:`built_in_renderers` for more general information about -renderers, including Chameleon ZPT renderers. - -.. index:: - single: sample template - -A Sample ZPT Template -~~~~~~~~~~~~~~~~~~~~~ - -Here's what a simple :term:`Chameleon` ZPT template used under -:app:`Pyramid` might look like: - -.. code-block:: xml - :linenos: - - <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> - <html xmlns="http://www.w3.org/1999/xhtml" - xmlns:tal="http://xml.zope.org/namespaces/tal"> - <head> - <meta http-equiv="content-type" content="text/html; charset=utf-8" /> - <title>${project} Application</title> - </head> - <body> - <h1 class="title">Welcome to <code>${project}</code>, an - application generated by the <a - href="http://docs.pylonsproject.org/projects/pyramid/dev/" - >pyramid</a> web - application framework.</h1> - </body> - </html> - -Note the use of :term:`Genshi` -style ``${replacements}`` above. This -is one of the ways that :term:`Chameleon` ZPT differs from standard -ZPT. The above template expects to find a ``project`` key in the set -of keywords passed in to it via :func:`~pyramid.renderers.render` or -:func:`~pyramid.renderers.render_to_response`. Typical ZPT -attribute-based syntax (e.g. ``tal:content`` and ``tal:replace``) also -works in these templates. - -.. index:: - single: ZPT macros - single: Chameleon ZPT macros - -Using ZPT Macros in :app:`Pyramid` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a :term:`renderer` is used to render a template, :app:`Pyramid` makes at -least two top-level names available to the template by default: ``context`` -and ``request``. One of the common needs in ZPT-based templates is to use -one template's "macros" from within a different template. In Zope, this is -typically handled by retrieving the template from the ``context``. But the -context in :app:`Pyramid` is a :term:`resource` object, and templates cannot -usually be retrieved from resources. To use macros in :app:`Pyramid`, you -need to make the macro template itself available to the rendered template by -passing the macro template, or even the macro itself, *into* the rendered -template. To do this you can use the :func:`pyramid.renderers.get_renderer` -API to retrieve the macro template, and pass it into the template being -rendered via the dictionary returned by the view. For example, using a -:term:`view configuration` via a :class:`~pyramid.view.view_config` decorator -that uses a :term:`renderer`: - -.. code-block:: python - :linenos: - - from pyramid.renderers import get_renderer - from pyramid.view import view_config - - @view_config(renderer='templates/mytemplate.pt') - def my_view(request): - main = get_renderer('templates/master.pt').implementation() - return {'main':main} - -Where ``templates/master.pt`` might look like so: - -.. code-block:: xml - :linenos: - - <html xmlns="http://www.w3.org/1999/xhtml" - xmlns:tal="http://xml.zope.org/namespaces/tal" - xmlns:metal="http://xml.zope.org/namespaces/metal"> - <span metal:define-macro="hello"> - <h1> - Hello <span metal:define-slot="name">Fred</span>! - </h1> - </span> - </html> - -And ``templates/mytemplate.pt`` might look like so: - -.. code-block:: xml - :linenos: - - <html xmlns="http://www.w3.org/1999/xhtml" - xmlns:tal="http://xml.zope.org/namespaces/tal" - xmlns:metal="http://xml.zope.org/namespaces/metal"> - <span metal:use-macro="main.macros['hello']"> - <span metal:fill-slot="name">Chris</span> - </span> - </html> - -.. index:: - single: Chameleon text templates - -.. _chameleon_text_templates: - -Templating with :term:`Chameleon` Text Templates ------------------------------------------------- - -:app:`Pyramid` also allows for the use of templates which are -composed entirely of non-XML text via :term:`Chameleon`. To do so, -you can create templates that are entirely composed of text except for -``${name}`` -style substitution points. - -Here's an example usage of a Chameleon text template. Create a file -on disk named ``mytemplate.txt`` in your project's ``templates`` -directory with the following contents: - -.. code-block:: text - - Hello, ${name}! - -Then in your project's ``views.py`` module, you can create a view -which renders this template: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - - @view_config(renderer='templates/mytemplate.txt') - def my_view(request): - return {'name':'world'} - -When the template is rendered, it will show: - -.. code-block:: text - - Hello, world! - -If you'd rather use templates directly within a view callable (without -the indirection of using a renderer), see :ref:`chameleon_text_module` -for the API description. - -See also :ref:`built_in_renderers` for more general information about -renderers, including Chameleon text renderers. +The same set of system values are provided to templates rendered via a renderer +view configuration as those provided to templates rendered imperatively. See +:ref:`renderer_system_values`. .. index:: - single: template renderer side effects - -Side Effects of Rendering a Chameleon Template ----------------------------------------------- - -When a Chameleon template is rendered from a file, the templating -engine writes a file in the same directory as the template file itself -as a kind of cache, in order to do less work the next time the -template needs to be read from disk. If you see "strange" ``.py`` -files showing up in your ``templates`` directory (or otherwise -directly "next" to your templates), it is due to this feature. + pair: debugging; templates -If you're using a version control system such as Subversion, you -should configure it to ignore these files. Here's the contents of the -author's ``svn propedit svn:ignore .`` in each of my ``templates`` -directories. +.. _debugging_templates: -.. code-block:: text - - *.pt.py - *.txt.py - -Note that I always name my Chameleon ZPT template files with a ``.pt`` -extension and my Chameleon text template files with a ``.txt`` -extension so that these ``svn:ignore`` patterns work. - -.. _debug_templates_section: - -Nicer Exceptions in Chameleon Templates ---------------------------------------- +Debugging Templates +------------------- -The exceptions raised by Chameleon templates when a rendering fails -are sometimes less than helpful. :app:`Pyramid` allows you to -configure your application development environment so that exceptions -generated by Chameleon during template compilation and execution will -contain nicer debugging information. - -.. warning:: Template-debugging behavior is not recommended for - production sites as it slows renderings; it's usually - only desirable during development. - -In order to turn on template exception debugging, you can use an -environment variable setting or a configuration file setting. - -To use an environment variable, start your application under a shell -using the ``PYRAMID_DEBUG_TEMPLATES`` operating system environment -variable set to ``1``, For example: - -.. code-block:: text - - $ PYRAMID_DEBUG_TEMPLATES=1 bin/paster serve myproject.ini - -To use a setting in the application ``.ini`` file for the same -purpose, set the ``debug_templates`` key to ``true`` within the -application's configuration section, e.g.: - -.. code-block:: ini - :linenos: - - [app:MyProject] - use = egg:MyProject#app - debug_templates = true - -With template debugging off, a :exc:`NameError` exception resulting -from rendering a template with an undefined variable -(e.g. ``${wrong}``) might end like this: - -.. code-block:: text - - File "...", in __getitem__ - raise NameError(key) - NameError: wrong - -Note that the exception has no information about which template was -being rendered when the error occured. But with template debugging -on, an exception resulting from the same problem might end like so: +A :exc:`NameError` exception resulting from rendering a template with an +undefined variable (e.g. ``${wrong}``) might end up looking like this: .. code-block:: text @@ -663,93 +375,9 @@ on, an exception resulting from the same problem might end like so: NameError: wrong -The latter tells you which template the error occurred in, as well as +The output tells you which template the error occurred in, as well as displaying the arguments passed to the template itself. -.. note:: - - Turning on ``debug_templates`` has the same effect as using the - Chameleon environment variable ``CHAMELEON_DEBUG``. See `Chameleon - Environment Variables - <http://chameleon.repoze.org/docs/latest/config.html#environment-variables>`_ - for more information. - -.. index:: - single: template internationalization - single: internationalization (of templates) - -:term:`Chameleon` Template Internationalization ------------------------------------------------ - -See :ref:`chameleon_translation_strings` for information about -supporting internationalized units of text within :term:`Chameleon` -templates. - -.. index:: - single: Mako - -.. _mako_templates: - -Templating With Mako Templates ------------------------------- - -:term:`Mako` is a templating system written by Mike Bayer. :app:`Pyramid` -has built-in bindings for the Mako templating system. The language -definition documentation for Mako templates is available from `the Mako -website <http://www.makotemplates.org/>`_. - -To use a Mako template, given a :term:`Mako` template file named ``foo.mak`` -in the ``templates`` subdirectory in your application package named -``mypackage``, you can configure the template as a :term:`renderer` like so: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - - @view_config(renderer='foo.mak') - def my_view(request): - return {'project':'my project'} - -For the above view callable to work, the following setting needs to be -present in the application stanza of your configuration's ``ini`` file: - -.. code-block:: ini - - mako.directories = mypackage:templates - -This lets the Mako templating system know that it should look for templates -in the ``templates`` subdirectory of the ``mypackage`` Python package. See -:ref:`mako_template_renderer_settings` for more information about the -``mako.directories`` setting and other Mako-related settings that can be -placed into the application's ``ini`` file. - -A Sample Mako Template -~~~~~~~~~~~~~~~~~~~~~~ - -Here's what a simple :term:`Mako` template used under :app:`Pyramid` might -look like: - -.. code-block:: xml - :linenos: - - <html> - <head> - <title>${project} Application</title> - </head> - <body> - <h1 class="title">Welcome to <code>${project}</code>, an - application generated by the <a - href="http://docs.pylonsproject.org/projects/pyramid/dev/" - >pyramid</a> web application framework.</h1> - </body> - </html> - -This template doesn't use any advanced features of Mako, only the -``${}`` replacement syntax for names that are passed in as -:term:`renderer globals`. See the `the Mako documentation -<http://www.makotemplates.org/>`_ to use more advanced features. - .. index:: single: automatic reloading of templates single: template automatic reload @@ -759,50 +387,71 @@ This template doesn't use any advanced features of Mako, only the Automatically Reloading Templates --------------------------------- -It's often convenient to see changes you make to a template file -appear immediately without needing to restart the application process. -:app:`Pyramid` allows you to configure your application development -environment so that a change to a template will be automatically -detected, and the template will be reloaded on the next rendering. +It's often convenient to see changes you make to a template file appear +immediately without needing to restart the application process. :app:`Pyramid` +allows you to configure your application development environment so that a +change to a template will be automatically detected, and the template will be +reloaded on the next rendering. -.. warning:: Auto-template-reload behavior is not recommended for - production sites as it slows rendering slightly; it's - usually only desirable during development. +.. warning:: + + Auto-template-reload behavior is not recommended for production sites as it + slows rendering slightly; it's usually only desirable during development. In order to turn on automatic reloading of templates, you can use an -environment variable, or a configuration file setting. +environment variable or a configuration file setting. -To use an environment variable, start your application under a shell -using the ``PYRAMID_RELOAD_TEMPLATES`` operating system environment -variable set to ``1``, For example: +To use an environment variable, start your application under a shell using the +``PYRAMID_RELOAD_TEMPLATES`` operating system environment variable set to +``1``, For example: .. code-block:: text - $ PYRAMID_RELOAD_TEMPLATES=1 bin/paster serve myproject.ini + $ PYRAMID_RELOAD_TEMPLATES=1 $VENV/bin/pserve myproject.ini -To use a setting in the application ``.ini`` file for the same -purpose, set the ``reload_templates`` key to ``true`` within the -application's configuration section, e.g.: +To use a setting in the application ``.ini`` file for the same purpose, set the +``pyramid.reload_templates`` key to ``true`` within the application's +configuration section, e.g.: .. code-block:: ini - :linenos: + :linenos: - [app:main] - use = egg:MyProject#app - reload_templates = true + [app:main] + use = egg:MyProject + pyramid.reload_templates = true .. index:: single: template system bindings + single: Chameleon single: Jinja2 + single: Mako .. _available_template_system_bindings: Available Add-On Template System Bindings ----------------------------------------- -Jinja2 template bindings are available for :app:`Pyramid` in the -``pyramid_jinja2`` package. You can get the latest release of -this package from the -`Python package index <http://pypi.python.org/pypi/pyramid_jinja2>`_ -(pypi). - +The Pylons Project maintains several packages providing bindings to different +templating languages including the following: + ++---------------------------+----------------------------+--------------------+ +| Template Language | Pyramid Bindings | Default Extensions | ++===========================+============================+====================+ +| Chameleon_ | pyramid_chameleon_ | .pt, .txt | ++---------------------------+----------------------------+--------------------+ +| Jinja2_ | pyramid_jinja2_ | .jinja2 | ++---------------------------+----------------------------+--------------------+ +| Mako_ | pyramid_mako_ | .mak, .mako | ++---------------------------+----------------------------+--------------------+ + +.. _Chameleon: http://chameleon.readthedocs.org/en/latest/ +.. _pyramid_chameleon: + http://docs.pylonsproject.org/projects/pyramid-chameleon/en/latest/ + +.. _Jinja2: http://jinja.pocoo.org/docs/ +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _Mako: http://www.makotemplates.org/ +.. _pyramid_mako: + http://docs.pylonsproject.org/projects/pyramid-mako/en/latest/ diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index bd45388c2..354a462d4 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -13,34 +13,32 @@ application. In this context, a "unit" is often a function or a method of a class instance. The unit is also referred to as a "unit under test". The goal of a single unit test is to test **only** some permutation of the -"unit under test". If you write a unit test that aims to verify the result -of a particular codepath through a Python function, you need only be -concerned about testing the code that *lives in the function body itself*. -If the function accepts a parameter that represents a complex application -"domain object" (such as a resource, a database connection, or an SMTP -server), the argument provided to this function during a unit test *need not -be* and likely *should not be* a "real" implementation object. For example, -although a particular function implementation may accept an argument that -represents an SMTP server object, and the function may call a method of this -object when the system is operating normally that would result in an email -being sent, a unit test of this codepath of the function does *not* need to -test that an email is actually sent. It just needs to make sure that the -function calls the method of the object provided as an argument that *would* -send an email if the argument happened to be the "real" implementation of an -SMTP server object. +"unit under test". If you write a unit test that aims to verify the result of +a particular codepath through a Python function, you need only be concerned +about testing the code that *lives in the function body itself*. If the +function accepts a parameter that represents a complex application "domain +object" (such as a resource, a database connection, or an SMTP server), the +argument provided to this function during a unit test *need not be* and likely +*should not be* a "real" implementation object. For example, although a +particular function implementation may accept an argument that represents an +SMTP server object, and the function may call a method of this object when the +system is operating normally that would result in an email being sent, a unit +test of this codepath of the function does *not* need to test that an email is +actually sent. It just needs to make sure that the function calls the method +of the object provided as an argument that *would* send an email if the +argument happened to be the "real" implementation of an SMTP server object. An *integration test*, on the other hand, is a different form of testing in which the interaction between two or more "units" is explicitly tested. -Integration tests verify that the components of your application work -together. You *might* make sure that an email was actually sent in an -integration test. +Integration tests verify that the components of your application work together. +You *might* make sure that an email was actually sent in an integration test. A *functional test* is a form of integration test in which the application is -run "literally". You would *have to* make sure that an email was actually -sent in a functional test, because it tests your code end to end. +run "literally". You would *have to* make sure that an email was actually sent +in a functional test, because it tests your code end to end. -It is often considered best practice to write each type of tests for any -given codebase. Unit testing often provides the opportunity to obtain better +It is often considered best practice to write each type of tests for any given +codebase. Unit testing often provides the opportunity to obtain better "coverage": it's usually possible to supply a unit under test with arguments and/or an environment which causes *all* of its potential codepaths to be executed. This is usually not as easy to do with a set of integration or @@ -52,12 +50,12 @@ The suggested mechanism for unit and integration testing of a :app:`Pyramid` application is the Python :mod:`unittest` module. Although this module is named :mod:`unittest`, it is actually capable of driving both unit and integration tests. A good :mod:`unittest` tutorial is available within `Dive -Into Python <http://diveintopython.org/unit_testing/index.html>`_ by Mark +Into Python <http://www.diveintopython.net/unit_testing/index.html>`_ by Mark Pilgrim. -:app:`Pyramid` provides a number of facilities that make unit, integration, -and functional tests easier to write. The facilities become particularly -useful when your code calls into :app:`Pyramid` -related framework functions. +:app:`Pyramid` provides a number of facilities that make unit, integration, and +functional tests easier to write. The facilities become particularly useful +when your code calls into :app:`Pyramid`-related framework functions. .. index:: single: test setup @@ -67,42 +65,41 @@ useful when your code calls into :app:`Pyramid` -related framework functions. .. _test_setup_and_teardown: Test Set Up and Tear Down --------------------------- +------------------------- :app:`Pyramid` uses a "global" (actually :term:`thread local`) data structure -to hold on to two items: the current :term:`request` and the current +to hold two items: the current :term:`request` and the current :term:`application registry`. These data structures are available via the :func:`pyramid.threadlocal.get_current_request` and -:func:`pyramid.threadlocal.get_current_registry` functions, respectively. -See :ref:`threadlocals_chapter` for information about these functions and the -data structures they return. +:func:`pyramid.threadlocal.get_current_registry` functions, respectively. See +:ref:`threadlocals_chapter` for information about these functions and the data +structures they return. If your code uses these ``get_current_*`` functions or calls :app:`Pyramid` code which uses ``get_current_*`` functions, you will need to call :func:`pyramid.testing.setUp` in your test setup and you will need to call :func:`pyramid.testing.tearDown` in your test teardown. -:func:`~pyramid.testing.setUp` pushes a registry onto the :term:`thread -local` stack, which makes the ``get_current_*`` functions work. It returns a +:func:`~pyramid.testing.setUp` pushes a registry onto the :term:`thread local` +stack, which makes the ``get_current_*`` functions work. It returns a :term:`Configurator` object which can be used to perform extra configuration required by the code under test. :func:`~pyramid.testing.tearDown` pops the thread local stack. -Normally when a Configurator is used directly with the ``main`` block of -a Pyramid application, it defers performing any "real work" until its -``.commit`` method is called (often implicitly by the -:meth:`pyramid.config.Configurator.make_wsgi_app` method). The -Configurator returned by :func:`~pyramid.testing.setUp` is an -*autocommitting* Configurator, however, which performs all actions -implied by methods called on it immediately. This is more convenient -for unit-testing purposes than needing to call -:meth:`pyramid.config.Configurator.commit` in each test after adding -extra configuration statements. +Normally when a Configurator is used directly with the ``main`` block of a +Pyramid application, it defers performing any "real work" until its ``.commit`` +method is called (often implicitly by the +:meth:`pyramid.config.Configurator.make_wsgi_app` method). The Configurator +returned by :func:`~pyramid.testing.setUp` is an *autocommitting* Configurator, +however, which performs all actions implied by methods called on it +immediately. This is more convenient for unit testing purposes than needing to +call :meth:`pyramid.config.Configurator.commit` in each test after adding extra +configuration statements. The use of the :func:`~pyramid.testing.setUp` and -:func:`~pyramid.testing.tearDown` functions allows you to supply each unit -test method in a test case with an environment that has an isolated registry -and an isolated request for the duration of a single test. Here's an example -of using this feature: +:func:`~pyramid.testing.tearDown` functions allows you to supply each unit test +method in a test case with an environment that has an isolated registry and an +isolated request for the duration of a single test. Here's an example of using +this feature: .. code-block:: python :linenos: @@ -117,21 +114,21 @@ of using this feature: def tearDown(self): testing.tearDown() -The above will make sure that -:func:`~pyramid.threadlocal.get_current_registry` called within a test -case method of ``MyTest`` will return the :term:`application registry` -associated with the ``config`` Configurator instance. Each test case -method attached to ``MyTest`` will use an isolated registry. +The above will make sure that :func:`~pyramid.threadlocal.get_current_registry` +called within a test case method of ``MyTest`` will return the +:term:`application registry` associated with the ``config`` Configurator +instance. Each test case method attached to ``MyTest`` will use an isolated +registry. The :func:`~pyramid.testing.setUp` and :func:`~pyramid.testing.tearDown` -functions accepts various arguments that influence the environment of the -test. See the :ref:`testing_module` chapter for information about the extra -arguments supported by these functions. +functions accept various arguments that influence the environment of the test. +See the :ref:`testing_module` API for information about the extra arguments +supported by these functions. -If you also want to make :func:`~pyramid.get_current_request` return something -other than ``None`` during the course of a single test, you can pass a -:term:`request` object into the :func:`pyramid.testing.setUp` within the -``setUp`` method of your test: +If you also want to make :func:`~pyramid.threadlocal.get_current_request` +return something other than ``None`` during the course of a single test, you +can pass a :term:`request` object into the :func:`pyramid.testing.setUp` within +the ``setUp`` method of your test: .. code-block:: python :linenos: @@ -147,15 +144,38 @@ other than ``None`` during the course of a single test, you can pass a def tearDown(self): testing.tearDown() -If you pass a :term:`request` object into :func:`pyramid.testing.setUp` -within your test case's ``setUp``, any test method attached to the -``MyTest`` test case that directly or indirectly calls +If you pass a :term:`request` object into :func:`pyramid.testing.setUp` within +your test case's ``setUp``, any test method attached to the ``MyTest`` test +case that directly or indirectly calls :func:`~pyramid.threadlocal.get_current_request` will receive the request object. Otherwise, during testing, -:func:`~pyramid.threadlocal.get_current_request` will return ``None``. -We use a "dummy" request implementation supplied by -:class:`pyramid.testing.DummyRequest` because it's easier to construct -than a "real" :app:`Pyramid` request object. +:func:`~pyramid.threadlocal.get_current_request` will return ``None``. We use a +"dummy" request implementation supplied by +:class:`pyramid.testing.DummyRequest` because it's easier to construct than a +"real" :app:`Pyramid` request object. + +Test setup using a context manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative style of setting up a test configuration is to use the ``with`` +statement and :func:`pyramid.testing.testConfig` to create a context manager. +The context manager will call :func:`pyramid.testing.setUp` before the code +under test and :func:`pyramid.testing.tearDown` afterwards. + +This style is useful for small self-contained tests. For example: + +.. code-block:: python + :linenos: + + import unittest + + class MyTest(unittest.TestCase): + + def test_my_function(self): + from pyramid import testing + with testing.testConfig() as config: + config.add_route('bar', '/bar/{id}') + my_function_which_needs_route_bar() What? ~~~~~ @@ -168,8 +188,8 @@ they're used by frameworks. Sorry. So here's a rule of thumb: if you don't about any of this, but you still want to write test code, just always call :func:`pyramid.testing.setUp` in your test's ``setUp`` method and :func:`pyramid.testing.tearDown` in your tests' ``tearDown`` method. This -won't really hurt anything if the application you're testing does not call -any ``get_current*`` function. +won't really hurt anything if the application you're testing does not call any +``get_current*`` function. .. index:: single: pyramid.testing @@ -178,7 +198,7 @@ any ``get_current*`` function. Using the ``Configurator`` and ``pyramid.testing`` APIs in Unit Tests --------------------------------------------------------------------- -The ``Configurator`` API and the ``pyramid.testing`` module provide a number +The ``Configurator`` API and the :mod:`pyramid.testing` module provide a number of functions which can be used during unit testing. These functions make :term:`configuration declaration` calls to the current :term:`application registry`, but typically register a "stub" or "dummy" feature in place of the @@ -190,24 +210,30 @@ function. .. code-block:: python :linenos: - from pyramid.security import has_permission - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden def view_fn(request): - if not has_permission('edit', request.context, request): - raise Forbidden + if request.has_permission('edit'): + raise HTTPForbidden return {'greeting':'hello'} +.. note:: + + This code implies that you have defined a renderer imperatively in a + relevant :class:`pyramid.config.Configurator` instance, otherwise it would + fail when run normally. + Without doing anything special during a unit test, the call to -:func:`~pyramid.security.has_permission` in this view function will always -return a ``True`` value. When a :app:`Pyramid` application starts normally, -it will populate a :term:`application registry` using :term:`configuration -declaration` calls made against a :term:`Configurator`. But if this -application registry is not created and populated (e.g. by initializing the -configurator with an authorization policy), like when you invoke application -code via a unit test, :app:`Pyramid` API functions will tend to either fail -or return default results. So how do you test the branch of the code in this -view function that raises :exc:`Forbidden`? +:meth:`~pyramid.request.Request.has_permission` in this view function will +always return a ``True`` value. When a :app:`Pyramid` application starts +normally, it will populate an :term:`application registry` using +:term:`configuration declaration` calls made against a :term:`Configurator`. +But if this application registry is not created and populated (e.g., by +initializing the configurator with an authorization policy), like when you +invoke application code via a unit test, :app:`Pyramid` API functions will tend +to either fail or return default results. So how do you test the branch of the +code in this view function that raises +:exc:`~pyramid.httpexceptions.HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -230,16 +256,15 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) request = testing.DummyRequest() request.context = testing.DummyResource() - self.assertRaises(Forbidden, view_fn, request) + self.assertRaises(HTTPForbidden, view_fn, request) def test_view_fn_allowed(self): - from pyramid.exceptions import Forbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=True) @@ -249,48 +274,51 @@ without needing to invoke the actual application configuration implied by its self.assertEqual(response, {'greeting':'hello'}) In the above example, we create a ``MyTest`` test case that inherits from -:mod:`unittest.TestCase`. If it's in our :app:`Pyramid` application, it will -be found when ``setup.py test`` is run. It has two test methods. +:class:`unittest.TestCase`. If it's in our :app:`Pyramid` application, it will +be found when ``py.test`` is run. It has two test methods. The first test method, ``test_view_fn_forbidden`` tests the ``view_fn`` when -the authentication policy forbids the current user the ``edit`` permission. -Its third line registers a "dummy" "non-permissive" authorization policy -using the :meth:`~pyramid.config.Configurator.testing_securitypolicy` method, -which is a special helper method for unit testing. - -We then create a :class:`pyramid.testing.DummyRequest` object which simulates -a WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a -request object that requires less setup than a "real" :app:`Pyramid` request. -We call the function being tested with the manufactured request. When the -function is called, :func:`pyramid.security.has_permission` will call the -"dummy" authentication policy we've registered through -:meth:`~pyramid.config.Configuration.testing_securitypolicy`, which denies -access. We check that the view function raises a :exc:`Forbidden` error. - -The second test method, named ``test_view_fn_allowed`` tests the alternate +the authentication policy forbids the current user the ``edit`` permission. Its +third line registers a "dummy" "non-permissive" authorization policy using the +:meth:`~pyramid.config.Configurator.testing_securitypolicy` method, which is a +special helper method for unit testing. + +We then create a :class:`pyramid.testing.DummyRequest` object which simulates a +WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a request +object that requires less setup than a "real" :app:`Pyramid` request. We call +the function being tested with the manufactured request. When the function is +called, :meth:`pyramid.request.Request.has_permission` will call the "dummy" +authentication policy we've registered through +:meth:`~pyramid.config.Configurator.testing_securitypolicy`, which denies +access. We check that the view function raises a +:exc:`~pyramid.httpexceptions.HTTPForbidden` error. + +The second test method, named ``test_view_fn_allowed``, tests the alternate case, where the authentication policy allows access. Notice that we pass -different values to -:meth:`~pyramid.config.Configurator.testing_securitypolicy` to obtain this -result. We assert at the end of this that the view function returns a value. +different values to :meth:`~pyramid.config.Configurator.testing_securitypolicy` +to obtain this result. We assert at the end of this that the view function +returns a value. Note that the test calls the :func:`pyramid.testing.setUp` function in its ``setUp`` method and the :func:`pyramid.testing.tearDown` function in its -``tearDown`` method. We assign the result of :func:`pyramid.testing.setUp` -as ``config`` on the unittest class. This is a :term:`Configurator` object -and all methods of the configurator can be called as necessary within -tests. If you use any of the :class:`~pyramid.config.Configurator` APIs during -testing, be sure to use this pattern in your test case's ``setUp`` and -``tearDown``; these methods make sure you're using a "fresh" -:term:`application registry` per test run. - -See the :ref:`testing_module` chapter for the entire :app:`Pyramid` -specific +``tearDown`` method. We assign the result of :func:`pyramid.testing.setUp` as +``config`` on the unittest class. This is a :term:`Configurator` object and +all methods of the configurator can be called as necessary within tests. If you +use any of the :class:`~pyramid.config.Configurator` APIs during testing, be +sure to use this pattern in your test case's ``setUp`` and ``tearDown``; these +methods make sure you're using a "fresh" :term:`application registry` per test +run. + +See the :ref:`testing_module` chapter for the entire :app:`Pyramid`-specific testing API. This chapter describes APIs for registering a security policy, -registering resources at paths, registering event listeners, registering -views and view permissions, and classes representing "dummy" implementations -of a request and a resource. +registering resources at paths, registering event listeners, registering views +and view permissions, and classes representing "dummy" implementations of a +request and a resource. + +.. seealso:: -See also the various methods of the :term:`Configurator` documented in -:ref:`configuration_module` that begin with the ``testing_`` prefix. + See also the various methods of the :term:`Configurator` documented in + :ref:`configuration_module` that begin with the ``testing_`` prefix. .. index:: single: integration tests @@ -301,66 +329,29 @@ Creating Integration Tests -------------------------- In :app:`Pyramid`, a *unit test* typically relies on "mock" or "dummy" -implementations to give the code under test only enough context to run. +implementations to give the code under test enough context to run. "Integration testing" implies another sort of testing. In the context of a -:app:`Pyramid`, integration test, the test logic tests the functionality of -some code *and* its integration with the rest of the :app:`Pyramid` +:app:`Pyramid` integration test, the test logic exercises the functionality of +the code under test *and* its integration with the rest of the :app:`Pyramid` framework. -In :app:`Pyramid` applications that are plugins to Pyramid, you can create an -integration test by including it's ``includeme`` function via -:meth:`pyramid.config.Configurator.include` in the test's setup code. This -causes the entire :app:`Pyramid` environment to be set up and torn down as if -your application was running "for real". This is a heavy-hammer way of -making sure that your tests have enough context to run properly, and it tests -your code's integration with the rest of :app:`Pyramid`. - -Let's demonstrate this by showing an integration test for a view. The below -test assumes that your application's package name is ``myapp``, and that -there is a ``views`` module in the app with a function with the name -``my_view`` in it that returns the response 'Welcome to this application' -after accessing some values that require a fully set up environment. - -.. code-block:: python - :linenos: +Creating an integration test for a :app:`Pyramid` application usually means +invoking the application's ``includeme`` function via +:meth:`pyramid.config.Configurator.include` within the test's setup code. This +causes the entire :app:`Pyramid` environment to be set up, simulating what +happens when your application is run "for real". This is a heavy-hammer way of +making sure that your tests have enough context to run properly, and tests your +code's integration with the rest of :app:`Pyramid`. - import unittest - - from pyramid import testing +.. seealso:: - class ViewIntegrationTests(unittest.TestCase): - def setUp(self): - """ This sets up the application registry with the - registrations your application declares in its ``includeme`` - function. - """ - import myapp - self.config = testing.setUp() - self.config.include('myapp') - - def tearDown(self): - """ Clear out the application registry """ - testing.tearDown() + See also :ref:`including_configuration` - def test_my_view(self): - from myapp.views import my_view - request = testing.DummyRequest() - result = my_view(request) - self.assertEqual(result.status, '200 OK') - body = result.app_iter[0] - self.failUnless('Welcome to' in body) - self.assertEqual(len(result.headerlist), 2) - self.assertEqual(result.headerlist[0], - ('Content-Type', 'text/html; charset=UTF-8')) - self.assertEqual(result.headerlist[1], ('Content-Length', - str(len(body)))) - -Unless you cannot avoid it, you should prefer writing unit tests that use the -:class:`~pyramid.config.Configurator` API to set up the right "mock" -registrations rather than creating an integration test. Unit tests will run -faster (because they do less for each test) and the result of a unit test is -usually easier to make assertions about. +Writing unit tests that use the :class:`~pyramid.config.Configurator` API to +set up the right "mock" registrations is often preferred to creating +integration tests. Unit tests will run faster (because they do less for each +test) and are usually easier to reason about. .. index:: single: functional tests @@ -372,34 +363,62 @@ Creating Functional Tests Functional tests test your literal application. -The below test assumes that your application's package name is ``myapp``, and -that there is view that returns an HTML body when the root URL is invoked. -It further assumes that you've added a ``tests_require`` dependency on the -``WebTest`` package within your ``setup.py`` file. :term:`WebTest` is a -functional testing package written by Ian Bicking. - -.. code-block:: python - :linenos: - - import unittest - - class FunctionalTests(unittest.TestCase): - def setUp(self): - from myapp import main - app = main({}) - from webtest import TestApp - self.testapp = TestApp(app) - - def test_root(self): - res = self.testapp.get('/', status=200) - self.failUnless('Pyramid' in res.body) - -When this test is run, each test creates a "real" WSGI application using the -``main`` function in your ``myapp.__init__`` module and uses :term:`WebTest` -to wrap that WSGI application. It assigns the result to ``self.testapp``. -In the test named ``test_root``, we use the testapp's ``get`` method to -invoke the root URL. We then assert that the returned HTML has the string -``Pyramid`` in it. - -See the :term:`WebTest` documentation for further information about the -methods available to a :class:`webtest.TestApp` instance. +In Pyramid, functional tests are typically written using the :term:`WebTest` +package, which provides APIs for invoking HTTP(S) requests to your application. +We also like ``py.test`` and ``pytest-cov`` to provide simple testing and +coverage reports. + +Regardless of which testing :term:`package` you use, be sure to add a +``tests_require`` dependency on that package to your application's ``setup.py`` +file. Using the project ``MyProject`` generated by the starter scaffold as +described in :doc:`project`, we would insert the following code immediately +following the ``requires`` block in the file ``MyProject/setup.py``. + +.. literalinclude:: MyProject/setup.py + :language: python + :linenos: + :lines: 11-22 + :lineno-start: 11 + :emphasize-lines: 8- + +Remember to change the dependency. + +.. literalinclude:: MyProject/setup.py + :language: python + :linenos: + :lines: 40-44 + :lineno-start: 40 + :emphasize-lines: 2-4 + +As always, whenever you change your dependencies, make sure to run the correct +``pip install -e`` command. + +.. code-block:: bash + + $VENV/bin/pip install -e ".[testing]" + +In your ``MyPackage`` project, your :term:`package` is named ``myproject`` +which contains a ``views`` module, which in turn contains a :term:`view` +function ``my_view`` that returns an HTML body when the root URL is invoked: + + .. literalinclude:: MyProject/myproject/views.py + :linenos: + :language: python + +The following example functional test demonstrates invoking the above +:term:`view`: + + .. literalinclude:: MyProject/myproject/tests.py + :linenos: + :pyobject: FunctionalTests + :language: python + +When this test is run, each test method creates a "real" :term:`WSGI` +application using the ``main`` function in your ``myproject.__init__`` module, +using :term:`WebTest` to wrap that WSGI application. It assigns the result to +``self.testapp``. In the test named ``test_root``, the ``TestApp``'s ``GET`` +method is used to invoke the root URL. Finally, an assertion is made that the +returned HTML contains the text ``Pyramid``. + +See the :term:`WebTest` documentation for further information about the methods +available to a :class:`webtest.app.TestApp` instance. diff --git a/docs/narr/threadlocals.rst b/docs/narr/threadlocals.rst index 909f643a0..7437a3a76 100644 --- a/docs/narr/threadlocals.rst +++ b/docs/narr/threadlocals.rst @@ -8,153 +8,137 @@ Thread Locals ============= -A :term:`thread local` variable is a variable that appears to be a -"global" variable to an application which uses it. However, unlike a -true global variable, one thread or process serving the application -may receive a different value than another thread or process when that -variable is "thread local". +A :term:`thread local` variable is a variable that appears to be a "global" +variable to an application which uses it. However, unlike a true global +variable, one thread or process serving the application may receive a different +value than another thread or process when that variable is "thread local". -When a request is processed, :app:`Pyramid` makes two :term:`thread -local` variables available to the application: a "registry" and a -"request". +When a request is processed, :app:`Pyramid` makes two :term:`thread local` +variables available to the application: a "registry" and a "request". Why and How :app:`Pyramid` Uses Thread Local Variables ---------------------------------------------------------- - -How are thread locals beneficial to :app:`Pyramid` and application -developers who use :app:`Pyramid`? Well, usually they're decidedly -**not**. Using a global or a thread local variable in any application -usually makes it a lot harder to understand for a casual reader. Use -of a thread local or a global is usually just a way to avoid passing -some value around between functions, which is itself usually a very -bad idea, at least if code readability counts as an important concern. - -For historical reasons, however, thread local variables are indeed -consulted by various :app:`Pyramid` API functions. For example, -the implementation of the :mod:`pyramid.security` function named -:func:`~pyramid.security.authenticated_userid` retrieves the thread -local :term:`application registry` as a matter of course to find an +------------------------------------------------------ + +How are thread locals beneficial to :app:`Pyramid` and application developers +who use :app:`Pyramid`? Well, usually they're decidedly **not**. Using a +global or a thread local variable in any application usually makes it a lot +harder to understand for a casual reader. Use of a thread local or a global is +usually just a way to avoid passing some value around between functions, which +is itself usually a very bad idea, at least if code readability counts as an +important concern. + +For historical reasons, however, thread local variables are indeed consulted by +various :app:`Pyramid` API functions. For example, the implementation of the +:mod:`pyramid.security` function named +:func:`~pyramid.security.authenticated_userid` (deprecated as of 1.5) retrieves +the thread local :term:`application registry` as a matter of course to find an :term:`authentication policy`. It uses the -:func:`pyramid.threadlocal.get_current_registry` function to -retrieve the application registry, from which it looks up the -authentication policy; it then uses the authentication policy to -retrieve the authenticated user id. This is how :app:`Pyramid` -allows arbitrary authentication policies to be "plugged in". - -When they need to do so, :app:`Pyramid` internals use two API -functions to retrieve the :term:`request` and :term:`application -registry`: :func:`~pyramid.threadlocal.get_current_request` and -:func:`~pyramid.threadlocal.get_current_registry`. The former -returns the "current" request; the latter returns the "current" -registry. Both ``get_current_*`` functions retrieve an object from a -thread-local data structure. These API functions are documented in -:ref:`threadlocal_module`. - -These values are thread locals rather than true globals because one -Python process may be handling multiple simultaneous requests or even -multiple :app:`Pyramid` applications. If they were true globals, -:app:`Pyramid` could not handle multiple simultaneous requests or -allow more than one :app:`Pyramid` application instance to exist in -a single Python process. - -Because one :app:`Pyramid` application is permitted to call -*another* :app:`Pyramid` application from its own :term:`view` code -(perhaps as a :term:`WSGI` app with help from the -:func:`pyramid.wsgi.wsgiapp2` decorator), these variables are -managed in a *stack* during normal system operations. The stack -instance itself is a `threading.local -<http://docs.python.org/library/threading.html#threading.local>`_. +:func:`pyramid.threadlocal.get_current_registry` function to retrieve the +application registry, from which it looks up the authentication policy; it then +uses the authentication policy to retrieve the authenticated user id. This is +how :app:`Pyramid` allows arbitrary authentication policies to be "plugged in". + +When they need to do so, :app:`Pyramid` internals use two API functions to +retrieve the :term:`request` and :term:`application registry`: +:func:`~pyramid.threadlocal.get_current_request` and +:func:`~pyramid.threadlocal.get_current_registry`. The former returns the +"current" request; the latter returns the "current" registry. Both +``get_current_*`` functions retrieve an object from a thread-local data +structure. These API functions are documented in :ref:`threadlocal_module`. + +These values are thread locals rather than true globals because one Python +process may be handling multiple simultaneous requests or even multiple +:app:`Pyramid` applications. If they were true globals, :app:`Pyramid` could +not handle multiple simultaneous requests or allow more than one :app:`Pyramid` +application instance to exist in a single Python process. + +Because one :app:`Pyramid` application is permitted to call *another* +:app:`Pyramid` application from its own :term:`view` code (perhaps as a +:term:`WSGI` app with help from the :func:`pyramid.wsgi.wsgiapp2` decorator), +these variables are managed in a *stack* during normal system operations. The +stack instance itself is a :class:`threading.local`. During normal operations, the thread locals stack is managed by a -:term:`Router` object. At the beginning of a request, the Router -pushes the application's registry and the request on to the stack. At -the end of a request, the stack is popped. The topmost request and -registry on the stack are considered "current". Therefore, when the -system is operating normally, the very definition of "current" is -defined entirely by the behavior of a pyramid :term:`Router`. +:term:`Router` object. At the beginning of a request, the Router pushes the +application's registry and the request on to the stack. At the end of a +request, the stack is popped. The topmost request and registry on the stack +are considered "current". Therefore, when the system is operating normally, +the very definition of "current" is defined entirely by the behavior of a +pyramid :term:`Router`. However, during unit testing, no Router code is ever invoked, and the -definition of "current" is defined by the boundary between calls to -the :meth:`pyramid.config.Configurator.begin` and -:meth:`pyramid.config.Configurator.end` methods (or between -calls to the :func:`pyramid.testing.setUp` and -:func:`pyramid.testing.tearDown` functions). These functions push -and pop the threadlocal stack when the system is under test. See -:ref:`test_setup_and_teardown` for the definitions of these functions. - -Scripts which use :app:`Pyramid` machinery but never actually start -a WSGI server or receive requests via HTTP such as scripts which use -the :mod:`pyramid.scripting` API will never cause any Router code -to be executed. However, the :mod:`pyramid.scripting` APIs also -push some values on to the thread locals stack as a matter of course. -Such scripts should expect the -:func:`~pyramid.threadlocal.get_current_request` function to always -return ``None``, and should expect the -:func:`~pyramid.threadlocal.get_current_registry` function to return -exactly the same :term:`application registry` for every request. +definition of "current" is defined by the boundary between calls to the +:meth:`pyramid.config.Configurator.begin` and +:meth:`pyramid.config.Configurator.end` methods (or between calls to the +:func:`pyramid.testing.setUp` and :func:`pyramid.testing.tearDown` functions). +These functions push and pop the threadlocal stack when the system is under +test. See :ref:`test_setup_and_teardown` for the definitions of these +functions. + +Scripts which use :app:`Pyramid` machinery but never actually start a WSGI +server or receive requests via HTTP, such as scripts which use the +:mod:`pyramid.scripting` API, will never cause any Router code to be executed. +However, the :mod:`pyramid.scripting` APIs also push some values on to the +thread locals stack as a matter of course. Such scripts should expect the +:func:`~pyramid.threadlocal.get_current_request` function to always return +``None``, and should expect the +:func:`~pyramid.threadlocal.get_current_registry` function to return exactly +the same :term:`application registry` for every request. Why You Shouldn't Abuse Thread Locals ------------------------------------- You probably should almost never use the :func:`~pyramid.threadlocal.get_current_request` or -:func:`~pyramid.threadlocal.get_current_registry` functions, except -perhaps in tests. In particular, it's almost always a mistake to use -``get_current_request`` or ``get_current_registry`` in application -code because its usage makes it possible to write code that can be -neither easily tested nor scripted. Inappropriate usage is defined as -follows: +:func:`~pyramid.threadlocal.get_current_registry` functions, except perhaps in +tests. In particular, it's almost always a mistake to use +``get_current_request`` or ``get_current_registry`` in application code because +its usage makes it possible to write code that can be neither easily tested nor +scripted. Inappropriate usage is defined as follows: - ``get_current_request`` should never be called within the body of a - :term:`view callable`, or within code called by a view callable. - View callables already have access to the request (it's passed in to - each as ``request``). - -- ``get_current_request`` should never be called in :term:`resource` code. - If a resource needs access to the request, it should be passed the request - by a :term:`view callable`. - -- ``get_current_request`` function should never be called because it's - "easier" or "more elegant" to think about calling it than to pass a - request through a series of function calls when creating some API - design. Your application should instead almost certainly pass data - derived from the request around rather than relying on being able to - call this function to obtain the request in places that actually - have no business knowing about it. Parameters are *meant* to be - passed around as function arguments, this is why they exist. Don't - try to "save typing" or create "nicer APIs" by using this function - in the place where a request is required; this will only lead to - sadness later. - -- Neither ``get_current_request`` nor ``get_current_registry`` should - ever be called within application-specific forks of third-party - library code. The library you've forked almost certainly has - nothing to do with :app:`Pyramid`, and making it dependent on - :app:`Pyramid` (rather than making your :mod:`pyramid` - application depend upon it) means you're forming a dependency in the - wrong direction. - -Use of the :func:`~pyramid.threadlocal.get_current_request` function -in application code *is* still useful in very limited circumstances. -As a rule of thumb, usage of ``get_current_request`` is useful -**within code which is meant to eventually be removed**. For -instance, you may find yourself wanting to deprecate some API that -expects to be passed a request object in favor of one that does not -expect to be passed a request object. But you need to keep -implementations of the old API working for some period of time while -you deprecate the older API. So you write a "facade" implementation -of the new API which calls into the code which implements the older -API. Since the new API does not require the request, your facade -implementation doesn't have local access to the request when it needs -to pass it into the older API implementation. After some period of -time, the older implementation code is disused and the hack that uses -``get_current_request`` is removed. This would be an appropriate -place to use the ``get_current_request``. - -Use of the :func:`~pyramid.threadlocal.get_current_registry` -function should be limited to testing scenarios. The registry made -current by use of the -:meth:`pyramid.config.Configurator.begin` method during a -test (or via :func:`pyramid.testing.setUp`) when you do not pass -one in is available to you via this API. - + :term:`view callable`, or within code called by a view callable. View + callables already have access to the request (it's passed in to each as + ``request``). + +- ``get_current_request`` should never be called in :term:`resource` code. If a + resource needs access to the request, it should be passed the request by a + :term:`view callable`. + +- ``get_current_request`` function should never be called because it's "easier" + or "more elegant" to think about calling it than to pass a request through a + series of function calls when creating some API design. Your application + should instead, almost certainly, pass around data derived from the request + rather than relying on being able to call this function to obtain the request + in places that actually have no business knowing about it. Parameters are + *meant* to be passed around as function arguments; this is why they exist. + Don't try to "save typing" or create "nicer APIs" by using this function in + the place where a request is required; this will only lead to sadness later. + +- Neither ``get_current_request`` nor ``get_current_registry`` should ever be + called within application-specific forks of third-party library code. The + library you've forked almost certainly has nothing to do with :app:`Pyramid`, + and making it dependent on :app:`Pyramid` (rather than making your + :app:`pyramid` application depend upon it) means you're forming a dependency + in the wrong direction. + +Use of the :func:`~pyramid.threadlocal.get_current_request` function in +application code *is* still useful in very limited circumstances. As a rule of +thumb, usage of ``get_current_request`` is useful **within code which is meant +to eventually be removed**. For instance, you may find yourself wanting to +deprecate some API that expects to be passed a request object in favor of one +that does not expect to be passed a request object. But you need to keep +implementations of the old API working for some period of time while you +deprecate the older API. So you write a "facade" implementation of the new API +which calls into the code which implements the older API. Since the new API +does not require the request, your facade implementation doesn't have local +access to the request when it needs to pass it into the older API +implementation. After some period of time, the older implementation code is +disused and the hack that uses ``get_current_request`` is removed. This would +be an appropriate place to use the ``get_current_request``. + +Use of the :func:`~pyramid.threadlocal.get_current_registry` function should be +limited to testing scenarios. The registry made current by use of the +:meth:`pyramid.config.Configurator.begin` method during a test (or via +:func:`pyramid.testing.setUp`) when you do not pass one in is available to you +via this API. diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index e1715dc25..cd8395eac 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -3,22 +3,30 @@ Traversal ========= +This chapter explains the technical details of how traversal works in Pyramid. + +For a quick example, see :doc:`hellotraversal`. + +For more about *why* you might use traversal, see :doc:`muchadoabouttraversal`. + A :term:`traversal` uses the URL (Universal Resource Locator) to find a -:term:`resource` located in a :term:`resource tree`, which is a set of -nested dictionary-like objects. Traversal is done by using each segment -of the path portion of the URL to navigate through the :term:`resource -tree`. You might think of this as looking up files and directories in a -file system. Traversal walks down the path until it finds a published -resource, analogous to a file system "directory" or "file". The -resource found as the result of a traversal becomes the -:term:`context` of the :term:`request`. Then, the :term:`view lookup` -subsystem is used to find some view code willing to "publish" this +:term:`resource` located in a :term:`resource tree`, which is a set of nested +dictionary-like objects. Traversal is done by using each segment of the path +portion of the URL to navigate through the :term:`resource tree`. You might +think of this as looking up files and directories in a file system. Traversal +walks down the path until it finds a published resource, analogous to a file +system "directory" or "file". The resource found as the result of a traversal +becomes the :term:`context` of the :term:`request`. Then, the :term:`view +lookup` subsystem is used to find some view code willing to "publish" this resource by generating a :term:`response`. -Using :term:`Traversal` to map a URL to code is optional. It is often -less easy to understand than :term:`URL dispatch`, so if you're a rank -beginner, it probably makes sense to use URL dispatch to map URLs to -code instead of traversal. In that case, you can skip this chapter. +.. note:: + + Using :term:`Traversal` to map a URL to code is optional. If you're creating + your first Pyramid application, it probably makes more sense to use + :term:`URL dispatch` to map URLs to code instead of traversal, as new Pyramid + developers tend to find URL dispatch slightly easier to understand. If you + use URL dispatch, you needn't read this chapter. .. index:: single: traversal details @@ -26,33 +34,32 @@ code instead of traversal. In that case, you can skip this chapter. Traversal Details ----------------- -:term:`Traversal` is dependent on information in a :term:`request` -object. Every :term:`request` object contains URL path information in -the ``PATH_INFO`` portion of the :term:`WSGI` environment. The -``PATH_INFO`` string is the portion of a request's URL following the -hostname and port number, but before any query string elements or -fragment element. For example the ``PATH_INFO`` portion of the URL -``http://example.com:8080/a/b/c?foo=1`` is ``/a/b/c``. +:term:`Traversal` is dependent on information in a :term:`request` object. +Every :term:`request` object contains URL path information in the ``PATH_INFO`` +portion of the :term:`WSGI` environment. The ``PATH_INFO`` string is the +portion of a request's URL following the hostname and port number, but before +any query string elements or fragment element. For example the ``PATH_INFO`` +portion of the URL ``http://example.com:8080/a/b/c?foo=1`` is ``/a/b/c``. -Traversal treats the ``PATH_INFO`` segment of a URL as a sequence of -path segments. For example, the ``PATH_INFO`` string ``/a/b/c`` is -converted to the sequence ``['a', 'b', 'c']``. +Traversal treats the ``PATH_INFO`` segment of a URL as a sequence of path +segments. For example, the ``PATH_INFO`` string ``/a/b/c`` is converted to the +sequence ``['a', 'b', 'c']``. -This path sequence is then used to descend through the :term:`resource -tree`, looking up a resource for each path segment. Each lookup uses the +This path sequence is then used to descend through the :term:`resource tree`, +looking up a resource for each path segment. Each lookup uses the ``__getitem__`` method of a resource in the tree. For example, if the path info sequence is ``['a', 'b', 'c']``: - :term:`Traversal` starts by acquiring the :term:`root` resource of the - application by calling the :term:`root factory`. The :term:`root factory` - can be configured to return whatever object is appropriate as the - traversal root of your application. + application by calling the :term:`root factory`. The :term:`root factory` can + be configured to return whatever object is appropriate as the traversal root + of your application. -- Next, the first element (``'a'``) is popped from the path segment - sequence and is used as a key to lookup the corresponding resource - in the root. This invokes the root resource's ``__getitem__`` method - using that value (``'a'``) as an argument. +- Next, the first element (``'a'``) is popped from the path segment sequence + and is used as a key to lookup the corresponding resource in the root. This + invokes the root resource's ``__getitem__`` method using that value (``'a'``) + as an argument. - If the root resource "contains" a resource with key ``'a'``, its ``__getitem__`` method will return it. The :term:`context` temporarily @@ -62,29 +69,26 @@ For example, if the path info sequence is ``['a', 'b', 'c']``: resource's ``__getitem__`` is called with that value (``'b'``) as an argument; we'll presume it succeeds. -- The "A" resource's ``__getitem__`` returns another resource, which - we'll call "B". The :term:`context` temporarily becomes the "B" - resource. +- The "A" resource's ``__getitem__`` returns another resource, which we'll call + "B". The :term:`context` temporarily becomes the "B" resource. -Traversal continues until the path segment sequence is exhausted or a -path element cannot be resolved to a resource. In either case, the -:term:`context` resource is the last object that the traversal -successfully resolved. If any resource found during traversal lacks a -``__getitem__`` method, or if its ``__getitem__`` method raises a -:exc:`KeyError`, traversal ends immediately, and that resource becomes -the :term:`context`. +Traversal continues until the path segment sequence is exhausted or a path +element cannot be resolved to a resource. In either case, the :term:`context` +resource is the last object that the traversal successfully resolved. If any +resource found during traversal lacks a ``__getitem__`` method, or if its +``__getitem__`` method raises a :exc:`KeyError`, traversal ends immediately, +and that resource becomes the :term:`context`. The results of a :term:`traversal` also include a :term:`view name`. If -traversal ends before the path segment sequence is exhausted, the -:term:`view name` is the *next* remaining path segment element. If the -:term:`traversal` expends all of the path segments, then the :term:`view -name` is the empty string (``''``). +traversal ends before the path segment sequence is exhausted, the :term:`view +name` is the *next* remaining path segment element. If the :term:`traversal` +expends all of the path segments, then the :term:`view name` is the empty +string (``''``). -The combination of the context resource and the :term:`view name` found -via traversal is used later in the same request by the :term:`view -lookup` subsystem to find a :term:`view callable`. How :app:`Pyramid` -performs view lookup is explained within the :ref:`view_config_chapter` -chapter. +The combination of the context resource and the :term:`view name` found via +traversal is used later in the same request by the :term:`view lookup` +subsystem to find a :term:`view callable`. How :app:`Pyramid` performs view +lookup is explained within the :ref:`view_config_chapter` chapter. .. index:: single: object tree @@ -96,20 +100,20 @@ chapter. The Resource Tree ----------------- -The resource tree is a set of nested dictionary-like resource objects -that begins with a :term:`root` resource. In order to use -:term:`traversal` to resolve URLs to code, your application must supply -a :term:`resource tree` to :app:`Pyramid`. +The resource tree is a set of nested dictionary-like resource objects that +begins with a :term:`root` resource. In order to use :term:`traversal` to +resolve URLs to code, your application must supply a :term:`resource tree` to +:app:`Pyramid`. In order to supply a root resource for an application the :app:`Pyramid` -:term:`Router` is configured with a callback known as a :term:`root -factory`. The root factory is supplied by the application, at startup -time, as the ``root_factory`` argument to the :term:`Configurator`. +:term:`Router` is configured with a callback known as a :term:`root factory`. +The root factory is supplied by the application at startup time as the +``root_factory`` argument to the :term:`Configurator`. -The root factory is a Python callable that accepts a :term:`request` -object, and returns the root object of the :term:`resource tree`. A -function, or class is typically used as an application's root factory. -Here's an example of a simple root factory class: +The root factory is a Python callable that accepts a :term:`request` object, +and returns the root object of the :term:`resource tree`. A function or class +is typically used as an application's root factory. Here's an example of a +simple root factory class: .. code-block:: python :linenos: @@ -126,82 +130,60 @@ passing it to an instance of a :term:`Configurator` named ``config``: config = Configurator(root_factory=Root) -The ``root_factory`` argument to the -:class:`~pyramid.config.Configurator` constructor registers this root -factory to be called to generate a root resource whenever a request -enters the application. The root factory registered this way is also -known as the global root factory. A root factory can alternately be -passed to the ``Configurator`` as a :term:`dotted Python name` which can -refer to a root factory defined in a different module. +The ``root_factory`` argument to the :class:`~pyramid.config.Configurator` +constructor registers this root factory to be called to generate a root +resource whenever a request enters the application. The root factory +registered this way is also known as the global root factory. A root factory +can alternatively be passed to the ``Configurator`` as a :term:`dotted Python +name` which can refer to a root factory defined in a different module. -If no :term:`root factory` is passed to the :app:`Pyramid` -:term:`Configurator` constructor, or if the ``root_factory`` value -specified is ``None``, a *default* root factory is used. The default -root factory always returns a resource that has no child resources; it -is effectively empty. +If no :term:`root factory` is passed to the :app:`Pyramid` :term:`Configurator` +constructor, or if the ``root_factory`` value specified is ``None``, a +:term:`default root factory` is used. The default root factory always returns +a resource that has no child resources; it is effectively empty. Usually a root factory for a traversal-based application will be more -complicated than the above ``Root`` class; in particular it may be -associated with a database connection or another persistence mechanism. - -.. sidebar:: Emulating the Default Root Factory - - For purposes of understanding the default root factory better, we'll note - that you can emulate the default root factory by using this code as an - explicit root factory in your application setup: - - .. code-block:: python - :linenos: - - class Root(object): - def __init__(self, request): - pass - - config = Configurator(root_factory=Root) - - The default root factory is just a really stupid object that has no - behavior or state. Using :term:`traversal` against an application that - uses the resource tree supplied by the default root resource is not very - interesting, because the default root resource has no children. Its - availability is more useful when you're developing an application using - :term:`URL dispatch`. +complicated than the above ``Root`` class. In particular it may be associated +with a database connection or another persistence mechanism. The above +``Root`` class is analogous to the default root factory present in Pyramid. The +default root factory is very simple and not very useful. .. note:: - If the items contained within the resource tree are "persistent" (they - have state that lasts longer than the execution of a single process), they - become analogous to the concept of :term:`domain model` objects used by - many other frameworks. + If the items contained within the resource tree are "persistent" (they have + state that lasts longer than the execution of a single process), they become + analogous to the concept of :term:`domain model` objects used by many other + frameworks. -The resource tree consists of *container* resources and *leaf* resources. -There is only one difference between a *container* resource and a *leaf* -resource: *container* resources possess a ``__getitem__`` method (making it +The resource tree consists of *container* resources and *leaf* resources. There +is only one difference between a *container* resource and a *leaf* resource: +*container* resources possess a ``__getitem__`` method (making it "dictionary-like") while *leaf* resources do not. The ``__getitem__`` method was chosen as the signifying difference between the two types of resources because the presence of this method is how Python itself typically determines whether an object is "containerish" or not (dictionary objects are "containerish"). -Each container resource is presumed to be willing to return a child resource -or raise a ``KeyError`` based on a name passed to its ``__getitem__``. +Each container resource is presumed to be willing to return a child resource or +raise a ``KeyError`` based on a name passed to its ``__getitem__``. -Leaf-level instances must not have a ``__getitem__``. If instances that -you'd like to be leaves already happen to have a ``__getitem__`` through some +Leaf-level instances must not have a ``__getitem__``. If instances that you'd +like to be leaves already happen to have a ``__getitem__`` through some historical inequity, you should subclass these resource types and cause their ``__getitem__`` methods to simply raise a ``KeyError``. Or just disuse them and think up another strategy. -Usually, the traversal root is a *container* resource, and as such it -contains other resources. However, it doesn't *need* to be a container. -Your resource tree can be as shallow or as deep as you require. +Usually the traversal root is a *container* resource, and as such it contains +other resources. However, it doesn't *need* to be a container. Your resource +tree can be as shallow or as deep as you require. -In general, the resource tree is traversed beginning at its root resource -using a sequence of path elements described by the ``PATH_INFO`` of the -current request; if there are path segments, the root resource's -``__getitem__`` is called with the next path segment, and it is expected to -return another resource. The resulting resource's ``__getitem__`` is called -with the very next path segment, and it is expected to return another -resource. This happens *ad infinitum* until all path segments are exhausted. +In general, the resource tree is traversed beginning at its root resource using +a sequence of path elements described by the ``PATH_INFO`` of the current +request. If there are path segments, the root resource's ``__getitem__`` is +called with the next path segment, and it is expected to return another +resource. The resulting resource's ``__getitem__`` is called with the very +next path segment, and it is expected to return another resource. This happens +*ad infinitum* until all path segments are exhausted. .. index:: single: traversal algorithm @@ -214,17 +196,17 @@ The Traversal Algorithm This section will attempt to explain the :app:`Pyramid` traversal algorithm. We'll provide a description of the algorithm, a diagram of how the algorithm -works, and some example traversal scenarios that might help you understand -how the algorithm operates against a specific resource tree. +works, and some example traversal scenarios that might help you understand how +the algorithm operates against a specific resource tree. We'll also talk a bit about :term:`view lookup`. The -:ref:`view_config_chapter` chapter discusses :term:`view lookup` in -detail, and it is the canonical source for information about views. -Technically, :term:`view lookup` is a :app:`Pyramid` subsystem that is -separated from traversal entirely. However, we'll describe the -fundamental behavior of view lookup in the examples in the next few -sections to give you an idea of how traversal and view lookup cooperate, -because they are almost always used together. +:ref:`view_config_chapter` chapter discusses :term:`view lookup` in detail, and +it is the canonical source for information about views. Technically, +:term:`view lookup` is a :app:`Pyramid` subsystem that is separated from +traversal entirely. However, we'll describe the fundamental behavior of view +lookup in the examples in the next few sections to give you an idea of how +traversal and view lookup cooperate, because they are almost always used +together. .. index:: single: view name @@ -233,26 +215,24 @@ because they are almost always used together. single: root factory single: default view -A Description of The Traversal Algorithm +A Description of the Traversal Algorithm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When a user requests a page from your traversal-powered application, the -system uses this algorithm to find a :term:`context` resource and a -:term:`view name`. +When a user requests a page from your traversal-powered application, the system +uses this algorithm to find a :term:`context` resource and a :term:`view name`. -#. The request for the page is presented to the :app:`Pyramid` - :term:`router` in terms of a standard :term:`WSGI` request, which is - represented by a WSGI environment and a WSGI ``start_response`` callable. +#. The request for the page is presented to the :app:`Pyramid` :term:`router` + in terms of a standard :term:`WSGI` request, which is represented by a WSGI + environment and a WSGI ``start_response`` callable. -#. The router creates a :term:`request` object based on the WSGI - environment. +#. The router creates a :term:`request` object based on the WSGI environment. -#. The :term:`root factory` is called with the :term:`request`. It returns - a :term:`root` resource. +#. The :term:`root factory` is called with the :term:`request`. It returns a + :term:`root` resource. #. The router uses the WSGI environment's ``PATH_INFO`` information to - determine the path segments to traverse. The leading slash is stripped - off ``PATH_INFO``, and the remaining path segments are split on the slash + determine the path segments to traverse. The leading slash is stripped off + ``PATH_INFO``, and the remaining path segments are split on the slash character to form a traversal sequence. The traversal algorithm by default attempts to first URL-unquote and then @@ -262,26 +242,26 @@ system uses this algorithm to find a :term:`context` resource and a Conversion from a URL-decoded string into Unicode is attempted using the UTF-8 encoding. If any URL-unquoted path segment in ``PATH_INFO`` is not decodeable using the UTF-8 decoding, a :exc:`TypeError` is raised. A - segment will be fully URL-unquoted and UTF8-decoded before it is passed - in to the ``__getitem__`` of any resource during traversal. + segment will be fully URL-unquoted and UTF8-decoded before it is passed in + to the ``__getitem__`` of any resource during traversal. - Thus, a request with a ``PATH_INFO`` variable of ``/a/b/c`` maps to the + Thus a request with a ``PATH_INFO`` variable of ``/a/b/c`` maps to the traversal sequence ``[u'a', u'b', u'c']``. -#. :term:`Traversal` begins at the root resource returned by the root - factory. For the traversal sequence ``[u'a', u'b', u'c']``, the root - resource's ``__getitem__`` is called with the name ``'a'``. Traversal - continues through the sequence. In our example, if the root resource's - ``__getitem__`` called with the name ``a`` returns a resource (aka +#. :term:`Traversal` begins at the root resource returned by the root factory. + For the traversal sequence ``[u'a', u'b', u'c']``, the root resource's + ``__getitem__`` is called with the name ``'a'``. Traversal continues + through the sequence. In our example, if the root resource's + ``__getitem__`` called with the name ``a`` returns a resource (a.k.a. resource "A"), that resource's ``__getitem__`` is called with the name ``'b'``. If resource "A" returns a resource "B" when asked for ``'b'``, resource B's ``__getitem__`` is then asked for the name ``'c'``, and may return resource "C". -#. Traversal ends when a) the entire path is exhausted or b) when any - resouce raises a :exc:`KeyError` from its ``__getitem__`` or c) when any +#. Traversal ends when either (a) the entire path is exhausted, (b) when any + resource raises a :exc:`KeyError` from its ``__getitem__``, (c) when any non-final path element traversal does not have a ``__getitem__`` method - (resulting in a :exc:`AttributeError`) or d) when any path element is + (resulting in an :exc:`AttributeError`), or (d) when any path element is prefixed with the set of characters ``@@`` (indicating that the characters following the ``@@`` token should be treated as a :term:`view name`). @@ -289,13 +269,13 @@ system uses this algorithm to find a :term:`context` resource and a resource found during traversal is deemed to be the :term:`context`. If the path has been exhausted when traversal ends, the :term:`view name` is deemed to be the empty string (``''``). However, if the path was *not* - exhausted before traversal terminated, the first remaining path segment - is treated as the view name. + exhausted before traversal terminated, the first remaining path segment is + treated as the view name. #. Any subsequent path elements after the :term:`view name` is found are deemed the :term:`subpath`. The subpath is always a sequence of path - segments that come from ``PATH_INFO`` that are "left over" after - traversal has completed. + segments that come from ``PATH_INFO`` that are "left over" after traversal + has completed. Once the :term:`context` resource, the :term:`view name`, and associated attributes such as the :term:`subpath` are located, the job of @@ -307,20 +287,19 @@ The traversal algorithm exposes two special cases: - You will often end up with a :term:`view name` that is the empty string as the result of a particular traversal. This indicates that the view lookup - machinery should look up the :term:`default view`. The default view is a - view that is registered with no name or a view which is registered with a - name that equals the empty string. + machinery should lookup the :term:`default view`. The default view is a view + that is registered with no name or a view which is registered with a name + that equals the empty string. -- If any path segment element begins with the special characters ``@@`` - (think of them as goggles), the value of that segment minus the goggle - characters is considered the :term:`view name` immediately and traversal - stops there. This allows you to address views that may have the same names - as resource names in the tree unambiguously. +- If any path segment element begins with the special characters ``@@`` (think + of them as goggles), the value of that segment minus the goggle characters is + considered the :term:`view name` immediately and traversal stops there. This + allows you to address views that may have the same names as resource names in + the tree unambiguously. Finally, traversal is responsible for locating a :term:`virtual root`. A -virtual root is used during "virtual hosting"; see the -:ref:`vhosting_chapter` chapter for information. We won't speak more about -it in this chapter. +virtual root is used during "virtual hosting". See the :ref:`vhosting_chapter` +chapter for information. We won't speak more about it in this chapter. .. image:: resourcetreetraverser.png @@ -331,13 +310,13 @@ Traversal Algorithm Examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ No one can be expected to understand the traversal algorithm by analogy and -description alone, so let's examine some traversal scenarios that use -concrete URLs and resource tree compositions. +description alone, so let's examine some traversal scenarios that use concrete +URLs and resource tree compositions. -Let's pretend the user asks for -``http://example.com/foo/bar/baz/biz/buz.txt``. The request's ``PATH_INFO`` -in that case is ``/foo/bar/baz/biz/buz.txt``. Let's further pretend that -when this request comes in that we're traversing the following resource tree: +Let's pretend the user asks for ``http://example.com/foo/bar/baz/biz/buz.txt``. +The request's ``PATH_INFO`` in that case is ``/foo/bar/baz/biz/buz.txt``. +Let's further pretend that when this request comes in, we're traversing the +following resource tree: .. code-block:: text @@ -349,40 +328,39 @@ when this request comes in that we're traversing the following resource tree: Here's what happens: -- :mod:`traversal` traverses the root, and attempts to find "foo", which it +- :term:`traversal` traverses the root, and attempts to find "foo", which it finds. -- :mod:`traversal` traverses "foo", and attempts to find "bar", which it +- :term:`traversal` traverses "foo", and attempts to find "bar", which it finds. -- :mod:`traversal` traverses "bar", and attempts to find "baz", which it does - not find (the "bar" resource raises a :exc:`KeyError` when asked for - "baz"). +- :term:`traversal` traverses "bar", and attempts to find "baz", which it does + not find (the "bar" resource raises a :exc:`KeyError` when asked for "baz"). The fact that it does not find "baz" at this point does not signify an error -condition. It signifies that: +condition. It signifies the following: -- the :term:`context` is the "bar" resource (the context is the last resource +- The :term:`context` is the "bar" resource (the context is the last resource found during traversal). -- the :term:`view name` is ``baz`` +- The :term:`view name` is ``baz``. -- the :term:`subpath` is ``('biz', 'buz.txt')`` +- The :term:`subpath` is ``('biz', 'buz.txt')``. At this point, traversal has ended, and :term:`view lookup` begins. Because it's the "context" resource, the view lookup machinery examines "bar" -to find out what "type" it is. Let's say it finds that the context is a -``Bar`` type (because "bar" happens to be an instance of the class ``Bar``). -Using the :term:`view name` (``baz``) and the type, view lookup asks the +to find out what "type" it is. Let's say it finds that the context is a ``Bar`` +type (because "bar" happens to be an instance of the class ``Bar``). Using the +:term:`view name` (``baz``) and the type, view lookup asks the :term:`application registry` this question: - Please find me a :term:`view callable` registered using a :term:`view configuration` with the name "baz" that can be used for the class ``Bar``. -Let's say that view lookup finds no matching view type. In this -circumstance, the :app:`Pyramid` :term:`router` returns the result of the -:term:`not found view` and the request ends. +Let's say that view lookup finds no matching view type. In this circumstance, +the :app:`Pyramid` :term:`router` returns the result of the :term:`Not Found +View` and the request ends. However, for this tree: @@ -400,61 +378,155 @@ However, for this tree: The user asks for ``http://example.com/foo/bar/baz/biz/buz.txt`` -- :mod:`traversal` traverses "foo", and attempts to find "bar", which it +- :term:`traversal` traverses "foo", and attempts to find "bar", which it finds. -- :mod:`traversal` traverses "bar", and attempts to find "baz", which it +- :term:`traversal` traverses "bar", and attempts to find "baz", which it finds. -- :mod:`traversal` traverses "baz", and attempts to find "biz", which it +- :term:`traversal` traverses "baz", and attempts to find "biz", which it finds. -- :mod:`traversal` traverses "biz", and attempts to find "buz.txt" which it +- :term:`traversal` traverses "biz", and attempts to find "buz.txt", which it does not find. The fact that it does not find a resource related to "buz.txt" at this point -does not signify an error condition. It signifies that: +does not signify an error condition. It signifies the following: -- the :term:`context` is the "biz" resource (the context is the last resource +- The :term:`context` is the "biz" resource (the context is the last resource found during traversal). -- the :term:`view name` is "buz.txt" +- The :term:`view name` is "buz.txt". -- the :term:`subpath` is an empty sequence ( ``()`` ). +- The :term:`subpath` is an empty sequence ( ``()`` ). At this point, traversal has ended, and :term:`view lookup` begins. Because it's the "context" resource, the view lookup machinery examines the "biz" resource to find out what "type" it is. Let's say it finds that the resource is a ``Biz`` type (because "biz" is an instance of the Python class -``Biz``). Using the :term:`view name` (``buz.txt``) and the type, view -lookup asks the :term:`application registry` this question: +``Biz``). Using the :term:`view name` (``buz.txt``) and the type, view lookup +asks the :term:`application registry` this question: - Please find me a :term:`view callable` registered with a :term:`view - configuration` with the name ``buz.txt`` that can be used for class - ``Biz``. + configuration` with the name ``buz.txt`` that can be used for class ``Biz``. -Let's say that question is answered by the application registry; in such a -situation, the application registry returns a :term:`view callable`. The -view callable is then called with the current :term:`WebOb` :term:`request` -as the sole argument: ``request``; it is expected to return a response. +Let's say that question is answered by the application registry. In such a +situation, the application registry returns a :term:`view callable`. The view +callable is then called with the current :term:`WebOb` :term:`request` as the +sole argument, ``request``. It is expected to return a response. -.. sidebar:: The Example View Callables Accept Only a Request; How Do I Access the Context Resource? +.. sidebar:: The Example View Callables Accept Only a Request; How Do I Access + the Context Resource? - Most of the examples in this book assume that a view callable is typically - passed only a :term:`request` object. Sometimes your view callables need - access to the :term:`context` resource, especially when you use - :term:`traversal`. You might use a supported alternate view callable + Most of the examples in this documentation assume that a view callable is + typically passed only a :term:`request` object. Sometimes your view + callables need access to the :term:`context` resource, especially when you + use :term:`traversal`. You might use a supported alternative view callable argument list in your view callables such as the ``(context, request)`` - calling convention described in - :ref:`request_and_context_view_definitions`. But you don't need to if you - don't want to. In view callables that accept only a request, the - :term:`context` resource found by traversal is available as the - ``context`` attribute of the request object, e.g. ``request.context``. - The :term:`view name` is available as the ``view_name`` attribute of the - request object, e.g. ``request.view_name``. Other :app:`Pyramid` - -specific request attributes are also available as described in - :ref:`special_request_attributes`. + calling convention described in :ref:`request_and_context_view_definitions`. + But you don't need to if you don't want to. In view callables that accept + only a request, the :term:`context` resource found by traversal is available + as the ``context`` attribute of the request object, e.g., + ``request.context``. The :term:`view name` is available as the ``view_name`` + attribute of the request object, e.g., ``request.view_name``. Other + :app:`Pyramid`-specific request attributes are also available as described + in :ref:`special_request_attributes`. + +.. index:: + single: resource interfaces + +.. _using_resource_interfaces: + +Using Resource Interfaces in View Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of registering your views with a ``context`` that names a Python +resource *class*, you can optionally register a view callable with a +``context`` which is an :term:`interface`. An interface can be attached +arbitrarily to any resource object. View lookup treats context interfaces +specially, and therefore the identity of a resource can be divorced from that +of the class which implements it. As a result, associating a view with an +interface can provide more flexibility for sharing a single view between two or +more different implementations of a resource type. For example, if two +resource objects of different Python class types share the same interface, you +can use the same view configuration to specify both of them as a ``context``. + +In order to make use of interfaces in your application during view dispatch, +you must create an interface and mark up your resource classes or instances +with interface declarations that refer to this interface. + +To attach an interface to a resource *class*, you define the interface and use +the :func:`zope.interface.implementer` class decorator to associate the +interface with the class. + +.. code-block:: python + :linenos: + + from zope.interface import Interface + from zope.interface import implementer + + class IHello(Interface): + """ A marker interface """ + + @implementer(IHello) + class Hello(object): + pass + +To attach an interface to a resource *instance*, you define the interface and +use the :func:`zope.interface.alsoProvides` function to associate the interface +with the instance. This function mutates the instance in such a way that the +interface is attached to it. + +.. code-block:: python + :linenos: + + from zope.interface import Interface + from zope.interface import alsoProvides + + class IHello(Interface): + """ A marker interface """ + + class Hello(object): + pass + + def make_hello(): + hello = Hello() + alsoProvides(hello, IHello) + return hello + +Regardless of how you associate an interface—with either a resource instance +or a resource class—the resulting code to associate that interface with a view +callable is the same. Assuming the above code that defines an ``IHello`` +interface lives in the root of your application, and its module is named +"resources.py", the interface declaration below will associate the +``mypackage.views.hello_world`` view with resources that implement, or provide, +this interface. + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + + config.add_view('mypackage.views.hello_world', name='hello.html', + context='mypackage.resources.IHello') + +Any time a resource that is determined to be the :term:`context` provides this +interface, and a view named ``hello.html`` is looked up against it as per the +URL, the ``mypackage.views.hello_world`` view callable will be invoked. + +Note, in cases where a view is registered against a resource class, and a view +is also registered against an interface that the resource class implements, an +ambiguity arises. Views registered for the resource class take precedence over +any views registered for any interface the resource class implements. Thus, if +one view configuration names a ``context`` of both the class type of a +resource, and another view configuration names a ``context`` of interface +implemented by the resource's class, and both view configurations are otherwise +identical, the view registered for the context's class will "win". + +For more information about defining resources with interfaces for use within +view configuration, see :ref:`resources_which_implement_interfaces`. + References ---------- @@ -468,6 +540,5 @@ See the :ref:`view_config_chapter` chapter for detailed information about The :mod:`pyramid.traversal` module contains API functions that deal with traversal, such as traversal invocation from within application code. -The :func:`pyramid.url.resource_url` function generates a URL when given a -resource retrieved from a resource tree. - +The :meth:`pyramid.request.Request.resource_url` method generates a URL when +given a resource retrieved from a resource tree. diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst new file mode 100644 index 000000000..fcdce4f8d --- /dev/null +++ b/docs/narr/upgrading.rst @@ -0,0 +1,248 @@ +.. _upgrading_chapter: + +Upgrading Pyramid +================= + +When a new version of Pyramid is released, it will sometimes deprecate a +feature or remove a feature that was deprecated in an older release. When +features are removed from Pyramid, applications that depend on those features +will begin to break. This chapter explains how to ensure your Pyramid +applications keep working when you upgrade the Pyramid version you're using. + +.. sidebar:: About Release Numbering + + Conventionally, application version numbering in Python is described as + ``major.minor.micro``. If your Pyramid version is "1.2.3", it means you're + running a version of Pyramid with the major version "1", the minor version + "2" and the micro version "3". A "major" release is one that increments the + first-dot number; 2.X.X might follow 1.X.X. A "minor" release is one that + increments the second-dot number; 1.3.X might follow 1.2.X. A "micro" + release is one that increments the third-dot number; 1.2.3 might follow + 1.2.2. In general, micro releases are "bugfix-only", and contain no new + features, minor releases contain new features but are largely backwards + compatible with older versions, and a major release indicates a large set of + backwards incompatibilities. + +The Pyramid core team is conservative when it comes to removing features. We +don't remove features unnecessarily, but we're human and we make mistakes which +cause some features to be evolutionary dead ends. Though we are willing to +support dead-end features for some amount of time, some eventually have to be +removed when the cost of supporting them outweighs the benefit of keeping them +around, because each feature in Pyramid represents a certain documentation and +maintenance burden. + +Deprecation and removal policy +------------------------------ + +When a feature is scheduled for removal from Pyramid or any of its official +add-ons, the core development team takes these steps: + +- Using the feature will begin to generate a `DeprecationWarning`, indicating + the version in which the feature became deprecated. + +- A note is added to the documentation indicating that the feature is + deprecated. + +- A note is added to the :ref:`changelog` about the deprecation. + +When a deprecated feature is eventually removed: + +- The feature is removed. + +- A note is added to the :ref:`changelog` about the removal. + +Features are never removed in *micro* releases. They are only removed in minor +and major releases. Deprecated features are kept around for at least *three* +minor releases from the time the feature became deprecated. Therefore, if a +feature is added in Pyramid 1.0, but it's deprecated in Pyramid 1.1, it will be +kept around through all 1.1.X releases, all 1.2.X releases and all 1.3.X +releases. It will finally be removed in the first 1.4.X release. + +Sometimes features are "docs-deprecated" instead of formally deprecated. This +means that the feature will be kept around indefinitely, but it will be removed +from the documentation or a note will be added to the documentation telling +folks to use some other newer feature. This happens when the cost of keeping +an old feature around is very minimal and the support and documentation burden +is very low. For example, we might rename a function that is an API without +changing the arguments it accepts. In this case, we'll often rename the +function, and change the docs to point at the new function name, but leave +around a backwards compatibility alias to the old function name so older code +doesn't break. + +"Docs deprecated" features tend to work "forever", meaning that they won't be +removed, and they'll never generate a deprecation warning. However, such +changes are noted in the :ref:`changelog`, so it's possible to know that you +should change older spellings to newer ones to ensure that people reading your +code can find the APIs you're using in the Pyramid docs. + + +Python support policy +~~~~~~~~~~~~~~~~~~~~~ + +At the time of a Pyramid version release, each supports all versions of Python +through the end of their lifespans. The end-of-life for a given version of +Python is when security updates are no longer released. + +- `Python 3.2 Lifespan <https://www.python.org/dev/peps/pep-0392/#lifespan>`_ + ends February 2016. +- `Python 3.3 Lifespan <https://www.python.org/dev/peps/pep-0392/#lifespan>`_ + ends September 2017. +- `Python 3.4 Lifespan <https://www.python.org/dev/peps/pep-0429/>`_ TBD. +- `Python 3.5 Lifespan <https://www.python.org/dev/peps/pep-0478/>`_ TBD. +- `Python 3.6 Lifespan <https://www.python.org/dev/peps/pep-0494/#id4>`_ + December 2021. + +To determine the Python support for a specific release of Pyramid, view its +``tox.ini`` file at the root of the repository's version. + + +Consulting the change history +----------------------------- + +Your first line of defense against application failures caused by upgrading to +a newer Pyramid release is always to read the :ref:`changelog` to find the +deprecations and removals for each release between the release you're currently +running and the one to which you wish to upgrade. The change history notes +every deprecation within a ``Deprecation`` section and every removal within a +``Backwards Incompatibilies`` section for each release. + +The change history often contains instructions for changing your code to avoid +deprecation warnings and how to change docs-deprecated spellings to newer ones. +You can follow along with each deprecation explanation in the change history, +simply doing a grep or other code search to your application, using the change +log examples to remediate each potential problem. + +.. _testing_under_new_release: + +Testing your application under a new Pyramid release +---------------------------------------------------- + +Once you've upgraded your application to a new Pyramid release and you've +remediated as much as possible by using the change history notes, you'll want +to run your application's tests (see :ref:`running_tests`) in such a way that +you can see DeprecationWarnings printed to the console when the tests run. + +.. code-block:: bash + + $ python -Wd setup.py test -q + +The ``-Wd`` argument tells Python to print deprecation warnings to the console. +See `the Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more information. + +As your tests run, deprecation warnings will be printed to the console +explaining the deprecation and providing instructions about how to prevent the +deprecation warning from being issued. For example: + +.. code-block:: bash + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + /home/chrism/projects/pyramid/env27/myproj/myproj/views.py:3: + DeprecationWarning: static: The "pyramid.view.static" class is deprecated + as of Pyramid 1.1; use the "pyramid.static.static_view" class instead with + the "use_subpath" argument set to True. + from pyramid.view import static + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + +In the above case, it's line #3 in the ``myproj.views`` module (``from +pyramid.view import static``) that is causing the problem: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + from pyramid.view import static + myview = static('static', 'static') + +The deprecation warning tells me how to fix it, so I can change the code to do +things the newer way: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + from pyramid.static import static_view + myview = static_view('static', 'static', use_subpath=True) + +When I run the tests again, the deprecation warning is no longer printed to my +console: + +.. code-block:: bash + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + + +My application doesn't have any tests or has few tests +------------------------------------------------------ + +If your application has no tests, or has only moderate test coverage, running +tests won't tell you very much, because the Pyramid codepaths that generate +deprecation warnings won't be executed. + +In this circumstance, you can start your application interactively under a +server run with the ``PYTHONWARNINGS`` environment variable set to ``default``. +On UNIX, you can do that via: + +.. code-block:: bash + + $ PYTHONWARNINGS=default $VENV/bin/pserve development.ini + +On Windows, you need to issue two commands: + +.. code-block:: bash + + C:\> set PYTHONWARNINGS=default + C:\> Scripts/pserve.exe development.ini + +At this point, it's ensured that deprecation warnings will be printed to the +console whenever a codepath is hit that generates one. You can then click +around in your application interactively to try to generate them, and remediate +as explained in :ref:`testing_under_new_release`. + +See `the PYTHONWARNINGS environment variable documentation +<http://docs.python.org/using/cmdline.html#envvar-PYTHONWARNINGS>`_ or `the +Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more information. + +Upgrading to the very latest Pyramid release +-------------------------------------------- + +When you upgrade your application to the most recent Pyramid release, +it's advisable to upgrade step-wise through each most recent minor release, +beginning with the one that you know your application currently runs under, +and ending on the most recent release. For example, if your application is +running in production on Pyramid 1.2.1, and the most recent Pyramid 1.3 +release is Pyramid 1.3.3, and the most recent Pyramid release is 1.4.4, it's +advisable to do this: + +- Upgrade your environment to the most recent 1.2 release. For example, the + most recent 1.2 release might be 1.2.3, so upgrade to it. Then run your + application's tests under 1.2.3 as described in + :ref:`testing_under_new_release`. Note any deprecation warnings and + remediate. + +- Upgrade to the most recent 1.3 release, 1.3.3. Run your application's tests, + note any deprecation warnings, and remediate. + +- Upgrade to 1.4.4. Run your application's tests, note any deprecation + warnings, and remediate. + +If you skip testing your application under each minor release (for example if +you upgrade directly from 1.2.1 to 1.4.4), you might miss a deprecation warning +and waste more time trying to figure out an error caused by a feature removal +than it would take to upgrade stepwise through each minor release. diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 5df1eb3af..c13558008 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -6,59 +6,40 @@ URL Dispatch ============ -:term:`URL dispatch` provides a simple way to map URLs to :term:`view` -code using a simple pattern matching language. An ordered set of -patterns is checked one-by-one. If one of the patterns matches the path -information associated with a request, a particular :term:`view -callable` is invoked. - -:term:`URL dispatch` is one of two ways to perform :term:`resource -location` in :app:`Pyramid`; the other way is using :term:`traversal`. -If no route is matched using :term:`URL dispatch`, :app:`Pyramid` falls -back to :term:`traversal` to handle the :term:`request`. - -It is the responsibility of the :term:`resource location` subsystem -(i.e., :term:`URL dispatch` or :term:`traversal`) to find the resource -object that is the :term:`context` of the :term:`request`. Once the -:term:`context` is determined, :term:`view lookup` is then responsible -for finding and invoking a :term:`view callable`. A view callable is a -specific bit of code, defined in your application, that receives the -:term:`request` and returns a :term:`response` object. - -Where appropriate, we will describe how view lookup interacts with -:term:`resource location`. The :ref:`view_config_chapter` chapter describes -the details of :term:`view lookup`. +:term:`URL dispatch` provides a simple way to map URLs to :term:`view` code +using a simple pattern matching language. An ordered set of patterns is +checked one by one. If one of the patterns matches the path information +associated with a request, a particular :term:`view callable` is invoked. A +view callable is a specific bit of code, defined in your application, that +receives the :term:`request` and returns a :term:`response` object. High-Level Operational Overview ------------------------------- -If route configuration is present in an application, the :app:`Pyramid` +If any route configuration is present in an application, the :app:`Pyramid` :term:`Router` checks every incoming request against an ordered set of URL matching patterns present in a *route map*. If any route pattern matches the information in the :term:`request`, -:app:`Pyramid` will invoke :term:`view lookup` using a :term:`context` -resource generated by the route match. +:app:`Pyramid` will invoke the :term:`view lookup` process to find a matching +view. -However, if no route pattern matches the information in the :term:`request` -provided to :app:`Pyramid`, it will fail over to using :term:`traversal` to -perform resource location and view lookup. +If no route pattern in the route map matches the information in the +:term:`request` provided in your application, :app:`Pyramid` will fail over to +using :term:`traversal` to perform resource location and view lookup. -Technically, URL dispatch is a :term:`resource location` mechanism (it finds -a context object). But ironically, using URL dispatch (instead of -:term:`traversal`) allows you to avoid thinking about your application in -terms of "resources" entirely, because it allows you to directly map a -:term:`view callable` to a route. +.. index:: + single: route configuration Route Configuration ------------------- :term:`Route configuration` is the act of adding a new :term:`route` to an -application. A route has a *name*, which acts as an identifier to be used -for URL generation. The name also allows developers to associate a view +application. A route has a *name*, which acts as an identifier to be used for +URL generation. The name also allows developers to associate a view configuration with the route. A route also has a *pattern*, meant to match against the ``PATH_INFO`` portion of a URL (the portion following the scheme -and port, e.g. ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``). It +and port, e.g., ``/foo/bar`` in the URL ``http://localhost:8080/foo/bar``). It also optionally has a ``factory`` and a set of :term:`route predicate` attributes. @@ -67,14 +48,13 @@ attributes. .. _config-add-route: -Configuring a Route via The ``add_route`` Configurator Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Configuring a Route to Match a View +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :meth:`pyramid.config.Configurator.add_route` method adds a single :term:`route configuration` to the :term:`application registry`. Here's an example: -.. ignore-next-block .. code-block:: python # "config" below is presumed to be an instance of the @@ -84,106 +64,58 @@ example: config.add_route('myroute', '/prefix/{one}/{two}') config.add_view(myview, route_name='myroute') -.. versionchanged:: 1.0a4 - Prior to 1.0a4, routes allow for a marker starting with a ``:``, for - example ``/prefix/:one/:two``. This style is now deprecated - in favor of ``{}`` usage which allows for additional functionality. - -.. index:: - single: route configuration; view callable - -.. _add_route_view_config: - -Route Configuration That Names a View Callable -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. warning:: This section describes a feature which has been deprecated in - Pyramid 1.1 and higher. In order to reduce confusion and documentation - burden, passing view-related parameters to - :meth:`~pyramid.config.Configurator.add_route` is deprecated. - - In versions earlier than 1.1, a view was permitted to be connected to a - route using a set of ``view*`` parameters passed to the - :meth:`~pyramid.config.Configurator.add_route`. This was a shorthand - which replaced the need to perform a subsequent call to - :meth:`~pyramid.config.Configurator.add_view` as described in - :ref:`config-add-route`. For example, it was valid (and often recommended) - to do: - - .. code-block:: python - - config.add_route('home', '/', view='mypackage.views.myview', - view_renderer='some/renderer.pt') - - Instead of the equivalent: - - .. code-block:: python - - config.add_route('home', '/') - config.add_view('mypackage.views.myview', route_name='home') - renderer='some/renderer.pt') - - Passing ``view*`` arguments to ``add_route`` as shown in the first - example above is now deprecated in favor of connecting a view to a - predefined route via :meth:`~pyramid.config.Configurator.add_view` using - the route's ``route_name`` parameter, as shown in the second example - above. - - A deprecation warning is now issued when any view-related parameter is - passed to ``Configurator.add_route``. The recommended way to associate a - view with a route is documented in :ref:`config-add-route`. +When a :term:`view callable` added to the configuration by way of +:meth:`~pyramid.config.Configurator.add_view` becomes associated with a route +via its ``route_name`` predicate, that view callable will always be found and +invoked when the associated route pattern matches during a request. -When a route configuration declaration names a ``view`` attribute, the value -of the attribute will reference a :term:`view callable`. This view callable -will be invoked when the route matches. A view callable, as described in -:ref:`views_chapter`, is developer-supplied code that "does stuff" as the -result of a request. - -Here's an example route configuration that references a view callable: +More commonly, you will not use any ``add_view`` statements in your project's +"setup" code. You will instead use ``add_route`` statements, and use a +:term:`scan` to associate view callables with routes. For example, if this is +a portion of your project's ``__init__.py``: .. code-block:: python - :linenos: - # "config" below is presumed to be an instance of the - # pyramid.config.Configurator class; "myview" is assumed - # to be a "view callable" function - from myproject.views import myview - config.add_route('myroute', '/prefix/{one}/{two}', view=myview) + config.add_route('myroute', '/prefix/{one}/{two}') + config.scan('mypackage') -You can also pass a :term:`dotted Python name` as the ``view`` argument -rather than an actual callable: +Note that we don't call :meth:`~pyramid.config.Configurator.add_view` in this +setup code. However, the above :term:`scan` execution +``config.scan('mypackage')`` will pick up each :term:`configuration +decoration`, including any objects decorated with the +:class:`pyramid.view.view_config` decorator in the ``mypackage`` Python +package. For example, if you have a ``views.py`` in your package, a scan will +pick up any of its configuration decorators, so we can add one there that +references ``myroute`` as a ``route_name`` parameter: .. code-block:: python - :linenos: - # "config" below is presumed to be an instance of the - # pyramid.config.Configurator class; "myview" is assumed - # to be a "view callable" function - config.add_route('myroute', '/prefix/{one}/{two}', - view='myproject.views.myview') + from pyramid.view import view_config + from pyramid.response import Response -When a route configuration names a ``view`` attribute, the :term:`view -callable` named as that ``view`` attribute will always be found and invoked -when the associated route pattern matches during a request. + @view_config(route_name='myroute') + def myview(request): + return Response('OK') -See :meth:`pyramid.config.Configurator.add_route` for a description of -view-related arguments. +The above combination of ``add_route`` and ``scan`` is completely equivalent to +using the previous combination of ``add_route`` and ``add_view``. .. index:: single: route path pattern syntax .. _route_pattern_syntax: + Route Pattern Syntax ~~~~~~~~~~~~~~~~~~~~ -The syntax of the pattern matching language used by :app:`Pyramid` URL -dispatch in the *pattern* argument is straightforward; it is close to that of -the :term:`Routes` system used by :term:`Pylons`. +The syntax of the pattern matching language used by :app:`Pyramid` URL dispatch +in the *pattern* argument is straightforward. It is close to that of the +:term:`Routes` system used by :term:`Pylons`. -The *pattern* used in route configuration may start with a slash character. -If the pattern does not start with a slash character, an implicit slash will -be prepended to it at matching time. For example, the following patterns are +The *pattern* used in route configuration may start with a slash character. If +the pattern does not start with a slash character, an implicit slash will be +prepended to it at matching time. For example, the following patterns are equivalent: .. code-block:: text @@ -196,18 +128,34 @@ and: /{foo}/bar/baz -A pattern segment (an individual item between ``/`` characters in the -pattern) may either be a literal string (e.g. ``foo``) *or* it may be a -replacement marker (e.g. ``{foo}``) or a certain combination of both. A -replacement marker does not need to be preceded by a ``/`` character. +If a pattern is a valid URL it won't be matched against an incoming request. +Instead it can be useful for generating external URLs. See :ref:`External +routes <external_route_narr>` for details. + +A pattern segment (an individual item between ``/`` characters in the pattern) +may either be a literal string (e.g., ``foo``) *or* it may be a replacement +marker (e.g., ``{foo}``), or a certain combination of both. A replacement +marker does not need to be preceded by a ``/`` character. + +A replacement marker is in the format ``{name}``, where this means "accept any +characters up to the next slash character and use this as the ``name`` +:term:`matchdict` value." -A replacement marker is in the format ``{name}``, where this means "accept -any characters up to the next slash character and use this as the ``name`` -:term:`matchdict` value." A matchdict is the dictionary representing the -dynamic parts extracted from a URL based on the routing pattern. It is -available as ``request.matchdict``. For example, the following pattern -defines one literal segment (``foo``) and two replacement markers (``baz``, -and ``bar``): +A replacement marker in a pattern must begin with an uppercase or lowercase +ASCII letter or an underscore, and can be composed only of uppercase or +lowercase ASCII letters, underscores, and numbers. For example: ``a``, +``a_b``, ``_b``, and ``b9`` are all valid replacement marker names, but ``0a`` +is not. + +.. versionchanged:: 1.2 + A replacement marker could not start with an underscore until Pyramid 1.2. + Previous versions required that the replacement marker start with an + uppercase or lowercase letter. + +A matchdict is the dictionary representing the dynamic parts extracted from a +URL based on the routing pattern. It is available as ``request.matchdict``. +For example, the following pattern defines one literal segment (``foo``) and +two replacement markers (``baz``, and ``bar``): .. code-block:: text @@ -227,48 +175,47 @@ It will not match the following patterns however: foo/1/2/ -> No match (trailing slash) bar/abc/def -> First segment literal mismatch -The match for a segment replacement marker in a segment will be done only up -to the first non-alphanumeric character in the segment in the pattern. So, -for instance, if this route pattern was used: +The match for a segment replacement marker in a segment will be done only up to +the first non-alphanumeric character in the segment in the pattern. So, for +instance, if this route pattern was used: .. code-block:: text foo/{name}.html -The literal path ``/foo/biz.html`` will match the above route pattern, and -the match result will be ``{'name':u'biz'}``. However, the literal path -``/foo/biz`` will not match, because it does not contain a literal ``.html`` -at the end of the segment represented by ``{name}.html`` (it only contains +The literal path ``/foo/biz.html`` will match the above route pattern, and the +match result will be ``{'name':u'biz'}``. However, the literal path +``/foo/biz`` will not match, because it does not contain a literal ``.html`` at +the end of the segment represented by ``{name}.html`` (it only contains ``biz``, not ``biz.html``). To capture both segments, two replacement markers can be used: .. code-block:: text - + foo/{name}.{ext} -The literal path ``/foo/biz.html`` will match the above route pattern, and -the match result will be ``{'name': 'biz', 'ext': 'html'}``. This occurs -because there is a literal part of ``.`` (period) between the two replacement -markers ``{name}`` and ``{ext}``. +The literal path ``/foo/biz.html`` will match the above route pattern, and the +match result will be ``{'name': 'biz', 'ext': 'html'}``. This occurs because +there is a literal part of ``.`` (period) between the two replacement markers +``{name}`` and ``{ext}``. Replacement markers can optionally specify a regular expression which will be -used to decide whether a path segment should match the marker. To specify -that a replacement marker should match only a specific set of characters as -defined by a regular expression, you must use a slightly extended form of -replacement marker syntax. Within braces, the replacement marker name must -be followed by a colon, then directly thereafter, the regular expression. -The *default* regular expression associated with a replacement marker -``[^/]+`` matches one or more characters which are not a slash. For example, -under the hood, the replacement marker ``{foo}`` can more verbosely be -spelled as ``{foo:[^/]+}``. You can change this to be an arbitrary regular -expression to match an arbitrary sequence of characters, such as -``{foo:\d+}`` to match only digits. +used to decide whether a path segment should match the marker. To specify that +a replacement marker should match only a specific set of characters as defined +by a regular expression, you must use a slightly extended form of replacement +marker syntax. Within braces, the replacement marker name must be followed by +a colon, then directly thereafter, the regular expression. The *default* +regular expression associated with a replacement marker ``[^/]+`` matches one +or more characters which are not a slash. For example, under the hood, the +replacement marker ``{foo}`` can more verbosely be spelled as ``{foo:[^/]+}``. +You can change this to be an arbitrary regular expression to match an arbitrary +sequence of characters, such as ``{foo:\d+}`` to match only digits. It is possible to use two replacement markers without any literal characters between them, for instance ``/{foo}{bar}``. However, this would be a -nonsensical pattern without specifying a custom regular expression to -restrict what each marker captures. +nonsensical pattern without specifying a custom regular expression to restrict +what each marker captures. Segments must contain at least one character in order to match a segment replacement marker. For example, for the URL ``/abc/``: @@ -277,7 +224,7 @@ replacement marker. For example, for the URL ``/abc/``: - ``/{foo}/`` will match. -Note that values representing matched path segments will be url-unquoted and +Note that values representing matched path segments will be URL-unquoted and decoded from UTF-8 into Unicode within the matchdict. So for instance, the following pattern: @@ -289,7 +236,7 @@ When matching the following URL: .. code-block:: text - foo/La%20Pe%C3%B1a + http://example.com/foo/La%20Pe%C3%B1a The matchdict will look like so (the value is URL-decoded / UTF-8 decoded): @@ -297,10 +244,54 @@ The matchdict will look like so (the value is URL-decoded / UTF-8 decoded): {'bar':u'La Pe\xf1a'} +Literal strings in the path segment should represent the *decoded* value of the +``PATH_INFO`` provided to Pyramid. You don't want to use a URL-encoded value +or a bytestring representing the literal encoded as UTF-8 in the pattern. For +example, rather than this: + +.. code-block:: text + + /Foo%20Bar/{baz} + +You'll want to use something like this: + +.. code-block:: text + + /Foo Bar/{baz} + +For patterns that contain "high-order" characters in its literals, you'll want +to use a Unicode value as the pattern as opposed to any URL-encoded or +UTF-8-encoded value. For example, you might be tempted to use a bytestring +pattern like this: + +.. code-block:: text + + /La Pe\xc3\xb1a/{x} + +But this will either cause an error at startup time or it won't match properly. +You'll want to use a Unicode value as the pattern instead rather than raw +bytestring escapes. You can use a high-order Unicode value as the pattern by +using `Python source file encoding <http://www.python.org/dev/peps/pep-0263/>`_ +plus the "real" character in the Unicode pattern in the source, like so: + +.. code-block:: text + + /La Peña/{x} + +Or you can ignore source file encoding and use equivalent Unicode escape +characters in the pattern. + +.. code-block:: text + + /La Pe\xf1a/{x} + +Dynamic segment names cannot contain high-order characters, so this applies +only to literals in the pattern. + If the pattern has a ``*`` in it, the name which follows it is considered a "remainder match". A remainder match *must* come at the end of the pattern. -Unlike segment replacement markers, it does not need to be preceded by a -slash. For example: +Unlike segment replacement markers, it does not need to be preceded by a slash. +For example: .. code-block:: text @@ -310,15 +301,15 @@ The above pattern will match these URLs, generating the following matchdicts: .. code-block:: text - foo/1/2/ -> + foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':()} - foo/abc/def/a/b/c -> + foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle':(u'a', u'b', u'c')} Note that when a ``*stararg`` remainder match is matched, the value put into the matchdict is turned into a tuple of path segments representing the -remainder of the path. These path segments are url-unquoted and decoded from +remainder of the path. These path segments are URL-unquoted and decoded from UTF-8 into Unicode. For example, for the following pattern: .. code-block:: text @@ -342,15 +333,15 @@ split by segment. Changing the regular expression used to match a marker can also capture the remainder of the URL, for example: .. code-block:: text - + foo/{baz}/{bar}{fizzle:.*} The above pattern will match these URLs, generating the following matchdicts: .. code-block:: text - foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':()} - foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c')} + foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':u''} + foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c'} This occurs because the default regular expression for a marker is ``[^/]+`` which will match everything up to the first ``/``, while ``{fizzle:.*}`` will @@ -365,16 +356,15 @@ Route Declaration Ordering Route configuration declarations are evaluated in a specific order when a request enters the system. As a result, the order of route configuration -declarations is very important. - -The order that routes declarations are evaluated is the order in which they -are added to the application at startup time. This is unlike -:term:`traversal`, which depends on emergent behavior which happens as a -result of traversing a resource tree. +declarations is very important. The order in which route declarations are +evaluated is the order in which they are added to the application at startup +time. (This is unlike a different way of mapping URLs to code that +:app:`Pyramid` provides, named :term:`traversal`, which does not depend on +pattern ordering). For routes added via the :mod:`~pyramid.config.Configurator.add_route` method, -the order that routes are evaluated is the order in which they are added to -the configuration imperatively. +the order that routes are evaluated is the order in which they are added to the +configuration imperatively. For example, route configuration statements with the following patterns might be added in the following order: @@ -384,47 +374,12 @@ be added in the following order: members/{def} members/abc -In such a configuration, the ``members/abc`` pattern would *never* be -matched. This is because the match ordering will always match -``members/{def}`` first; the route configuration with ``members/abc`` will -never be evaluated. +In such a configuration, the ``members/abc`` pattern would *never* be matched. +This is because the match ordering will always match ``members/{def}`` first; +the route configuration with ``members/abc`` will never be evaluated. .. index:: - single: route factory - -.. _route_factories: - -Route Factories -~~~~~~~~~~~~~~~ - -A "route" configuration declaration can mention a "factory". When that route -matches a request, and a factory is attached to a route, the :term:`root -factory` passed at startup time to the :term:`Configurator` is ignored; -instead the factory associated with the route is used to generate a -:term:`root` object. This object will usually be used as the :term:`context` -resource of the view callable ultimately found via :term:`view lookup`. - -.. code-block:: python - :linenos: - - config.add_route('abc', '/abc', - factory='myproject.resources.root_factory') - config.add_view('myproject.views.theview', route_name='abc') - -The factory can either be a Python object or a :term:`dotted Python name` (a -string) which points to such a Python object, as it is above. - -In this way, each route can use a different factory, making it possible to -supply a different :term:`context` resource object to the view related to -each particular route. - -Supplying a different resource factory for each route is useful when you're -trying to use a :app:`Pyramid` :term:`authorization policy` to provide -declarative, "context sensitive" security checks; each resource can maintain -a separate :term:`ACL`, as documented in -:ref:`using_security_with_urldispatch`. It is also useful when you wish to -combine URL dispatch with :term:`traversal` as documented within -:ref:`hybrid_chapter`. + single: route configuration arguments Route Configuration Arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -439,171 +394,40 @@ the associated route to be considered a match during the route matching process. Examples of route predicate arguments are ``pattern``, ``xhr``, and ``request_method``. -Other arguments are view configuration related arguments. These only have an -effect when the route configuration names a ``view``. These arguments have -been deprecated as of :app:`Pyramid` 1.1 (see :ref:`add_route_view_config`). - Other arguments are ``name`` and ``factory``. These arguments represent neither predicates nor view configuration information. -.. _custom_route_predicates: - -Custom Route Predicates -~~~~~~~~~~~~~~~~~~~~~~~ - -Each of the predicate callables fed to the ``custom_predicates`` argument of -:meth:`~pyramid.config.Configurator.add_route` must be a callable accepting -two arguments. The first argument passed to a custom predicate is a -dictionary conventionally named ``info``. The second argument is the current -:term:`request` object. - -The ``info`` dictionary has a number of contained values: ``match`` is a -dictionary: it represents the arguments matched in the URL by the route. -``route`` is an object representing the route which was matched (see -:class:`pyramid.interfaces.IRoute` for the API of such a route object). - -``info['match']`` is useful when predicates need access to the route match. -For example: - -.. code-block:: python - :linenos: - - def any_of(segment_name, *allowed): - def predicate(info, request): - if info['match'][segment_name] in allowed: - return True - return predicate - - num_one_two_or_three = any_of('num', 'one', 'two', 'three') - - config.add_route('route_to_num', '/{num}', - custom_predicates=(num_one_two_or_three,)) - -The above ``any_of`` function generates a predicate which ensures that the -match value named ``segment_name`` is in the set of allowable values -represented by ``allowed``. We use this ``any_of`` function to generate a -predicate function named ``num_one_two_or_three``, which ensures that the -``num`` segment is one of the values ``one``, ``two``, or ``three`` , and use -the result as a custom predicate by feeding it inside a tuple to the -``custom_predicates`` argument to -:meth:`~pyramid.config.Configurator.add_route`. - -A custom route predicate may also *modify* the ``match`` dictionary. For -instance, a predicate might do some type conversion of values: - -.. code-block:: python - :linenos: - - def integers(*segment_names): - def predicate(info, request): - match = info['match'] - for segment_name in segment_names: - try: - match[segment_name] = int(match[segment_name]) - except (TypeError, ValueError): - pass - return True - return predicate - - ymd_to_int = integers('year', 'month', 'day') - - config.add_route('ymd', '/{year}/{month}/{day}', - custom_predicates=(ymd_to_int,)) - -Note that a conversion predicate is still a predicate so it must return -``True`` or ``False``; a predicate that does *only* conversion, such as the -one we demonstrate above should unconditionally return ``True``. - -To avoid the try/except uncertainty, the route pattern can contain regular -expressions specifying requirements for that marker. For instance: - -.. code-block:: python - :linenos: - - def integers(*segment_names): - def predicate(info, request): - match = info['match'] - for segment_name in segment_names: - match[segment_name] = int(match[segment_name]) - return True - return predicate - - ymd_to_int = integers('year', 'month', 'day') - - config.add_route('ymd', '/{year:\d+}/{month:\d+}/{day:\d+}', - custom_predicates=(ymd_to_int,)) - -Now the try/except is no longer needed because the route will not match at -all unless these markers match ``\d+`` which requires them to be valid digits -for an ``int`` type conversion. - -The ``match`` dictionary passed within ``info`` to each predicate attached to -a route will be the same dictionary. Therefore, when registering a custom -predicate which modifies the ``match`` dict, the code registering the -predicate should usually arrange for the predicate to be the *last* custom -predicate in the custom predicate list. Otherwise, custom predicates which -fire subsequent to the predicate which performs the ``match`` modification -will receive the *modified* match dictionary. - -.. warning:: - - It is a poor idea to rely on ordering of custom predicates to build a - conversion pipeline, where one predicate depends on the side effect of - another. For instance, it's a poor idea to register two custom - predicates, one which handles conversion of a value to an int, the next - which handles conversion of that integer to some custom object. Just do - all that in a single custom predicate. - -The ``route`` object in the ``info`` dict is an object that has two useful -attributes: ``name`` and ``pattern``. The ``name`` attribute is the route -name. The ``pattern`` attribute is the route pattern. An example of using -the route in a set of route predicates: - -.. code-block:: python - :linenos: - - def twenty_ten(info, request): - if info['route'].name in ('ymd', 'ym', 'y'): - return info['match']['year'] == '2010' - - config.add_route('y', '/{year}', custom_predicates=(twenty_ten,)) - config.add_route('ym', '/{year}/{month}', custom_predicates=(twenty_ten,)) - config.add_route('ymd', '/{year}/{month}/{day}', - custom_predicates=(twenty_ten,)) - -The above predicate, when added to a number of route configurations ensures -that the year match argument is '2010' if and only if the route name is -'ymd', 'ym', or 'y'. - -See also :class:`pyramid.interfaces.IRoute` for more API documentation about -route objects. +.. index:: + single: route matching Route Matching -------------- The main purpose of route configuration is to match (or not match) the ``PATH_INFO`` present in the WSGI environment provided during a request -against a URL path pattern. +against a URL path pattern. ``PATH_INFO`` represents the path portion of the +URL that was requested. The way that :app:`Pyramid` does this is very simple. When a request enters the system, for each route configuration declaration present in the system, -:app:`Pyramid` checks the ``PATH_INFO`` against the pattern declared. - -If any route matches, the route matching process stops. The :term:`request` -is decorated with a special :term:`interface` which describes it as a "route -request", the :term:`context` resource is generated, and the context and the -resulting request are handed off to :term:`view lookup`. During view lookup, -if a :term:`view callable` associated with the matched route is found, that -view is called. - -When a route configuration is declared, it may contain :term:`route -predicate` arguments. All route predicates associated with a route -declaration must be ``True`` for the route configuration to be used for a -given request. - -If any predicate in the set of :term:`route predicate` arguments provided to -a route configuration returns ``False``, that route is skipped and route -matching continues through the ordered set of routes. +:app:`Pyramid` checks the request's ``PATH_INFO`` against the pattern +declared. This checking happens in the order that the routes were declared +via :meth:`pyramid.config.Configurator.add_route`. + +When a route configuration is declared, it may contain :term:`route predicate` +arguments. All route predicates associated with a route declaration must be +``True`` for the route configuration to be used for a given request during a +check. If any predicate in the set of :term:`route predicate` arguments +provided to a route configuration returns ``False`` during a check, that route +is skipped and route matching continues through the ordered set of routes. + +If any route matches, the route matching process stops and the :term:`view +lookup` subsystem takes over to find the most reasonable view callable for the +matched route. Most often, there's only one view that will match (a view +configured with a ``route_name`` argument matching the matched route). To gain +a better understanding of how routes and views are associated in a real +application, you can use the ``pviews`` command, as documented in +:ref:`displaying_matching_views`. If no route matches after all route patterns are exhausted, :app:`Pyramid` falls back to :term:`traversal` to do :term:`resource location` and @@ -618,11 +442,10 @@ The Matchdict ~~~~~~~~~~~~~ When the URL pattern associated with a particular route configuration is -matched by a request, a dictionary named ``matchdict`` is added as an -attribute of the :term:`request` object. Thus, ``request.matchdict`` will -contain the values that match replacement patterns in the ``pattern`` -element. The keys in a matchdict will be strings. The values will be -Unicode objects. +matched by a request, a dictionary named ``matchdict`` is added as an attribute +of the :term:`request` object. Thus, ``request.matchdict`` will contain the +values that match replacement patterns in the ``pattern`` element. The keys in +a matchdict will be strings. The values will be Unicode objects. .. note:: @@ -639,10 +462,10 @@ The Matched Route When the URL pattern associated with a particular route configuration is matched by a request, an object named ``matched_route`` is added as an -attribute of the :term:`request` object. Thus, ``request.matched_route`` -will be an object implementing the :class:`~pyramid.interfaces.IRoute` -interface which matched the request. The most useful attribute of the route -object is ``name``, which is the name of the route that matched. +attribute of the :term:`request` object. Thus, ``request.matched_route`` will +be an object implementing the :class:`~pyramid.interfaces.IRoute` interface +which matched the request. The most useful attribute of the route object is +``name``, which is the name of the route that matched. .. note:: @@ -653,8 +476,8 @@ Routing Examples ---------------- Let's check out some examples of how route configuration statements might be -commonly declared, and what will happen if they are matched by the -information present in a request. +commonly declared, and what will happen if they are matched by the information +present in a request. .. _urldispatch_example1: @@ -665,35 +488,41 @@ The simplest route declaration which configures a route match to *directly* result in a particular view callable being invoked: .. code-block:: python - :linenos: + :linenos: config.add_route('idea', 'site/{id}') - config.add_view('mypackage.views.site_view', route_name='idea') + config.scan() When a route configuration with a ``view`` attribute is added to the system, and an incoming request matches the *pattern* of the route configuration, the :term:`view callable` named as the ``view`` attribute of the route configuration will be invoked. -In the case of the above example, when the URL of a request matches -``/site/{id}``, the view callable at the Python dotted path name -``mypackage.views.site_view`` will be called with the request. In other -words, we've associated a view callable directly with a route pattern. +Recall that the ``@view_config`` is equivalent to calling ``config.add_view``, +because the ``config.scan()`` call will import ``mypackage.views``, shown +below, and execute ``config.add_view`` under the hood. Each view then maps the +route name to the matching view callable. In the case of the above example, +when the URL of a request matches ``/site/{id}``, the view callable at the +Python dotted path name ``mypackage.views.site_view`` will be called with the +request. In other words, we've associated a view callable directly with a +route pattern. When the ``/site/{id}`` route pattern matches during a request, the -``site_view`` view callable is invoked with that request as its sole -argument. When this route matches, a ``matchdict`` will be generated and -attached to the request as ``request.matchdict``. If the specific URL -matched is ``/site/1``, the ``matchdict`` will be a dictionary with a single -key, ``id``; the value will be the string ``'1'``, ex.: ``{'id':'1'}``. +``site_view`` view callable is invoked with that request as its sole argument. +When this route matches, a ``matchdict`` will be generated and attached to the +request as ``request.matchdict``. If the specific URL matched is ``/site/1``, +the ``matchdict`` will be a dictionary with a single key, ``id``; the value +will be the string ``'1'``, ex.: ``{'id':'1'}``. The ``mypackage.views`` module referred to above might look like so: .. code-block:: python :linenos: + from pyramid.view import view_config from pyramid.response import Response + @view_config(route_name='idea') def site_view(request): return Response(request.matchdict['id']) @@ -706,20 +535,39 @@ information about views. Example 2 ~~~~~~~~~ -Below is an example of a more complicated set of route statements you might -add to your application: +Below is an example of a more complicated set of route statements you might add +to your application: .. code-block:: python :linenos: config.add_route('idea', 'ideas/{idea}') config.add_route('user', 'users/{user}') - config.add_route('tag', 'tags/{tags}') + config.add_route('tag', 'tags/{tag}') + config.scan() + +Here is an example of a corresponding ``mypackage.views`` module: - config.add_view('mypackage.views.idea_view', route_name='idea') - config.add_view('mypackage.views.user_view', route_name='user') - config.add_view('mypackage.views.tag_view', route_name='tag') +.. code-block:: python + :linenos: + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='idea') + def idea_view(request): + return Response(request.matchdict['id']) + + @view_config(route_name='user') + def user_view(request): + user = request.matchdict['user'] + return Response(u'The user is {}.'.format(user)) + + @view_config(route_name='tag') + def tag_view(request): + tag = request.matchdict['tag'] + return Response(u'The tag is {}.'.format(tag)) + The above configuration will allow :app:`Pyramid` to service URLs in these forms: @@ -735,33 +583,32 @@ forms: and attached to the :term:`request` will consist of ``{'idea':'1'}``. - When a URL matches the pattern ``/users/{user}``, the view callable - available at the dotted Python pathname ``mypackage.views.user_view`` will - be called. For the specific URL ``/users/1``, the ``matchdict`` generated - and attached to the :term:`request` will consist of ``{'user':'1'}``. + available at the dotted Python pathname ``mypackage.views.user_view`` will be + called. For the specific URL ``/users/1``, the ``matchdict`` generated and + attached to the :term:`request` will consist of ``{'user':'1'}``. - When a URL matches the pattern ``/tags/{tag}``, the view callable available at the dotted Python pathname ``mypackage.views.tag_view`` will be called. - For the specific URL ``/tags/1``, the ``matchdict`` generated and attached - to the :term:`request` will consist of ``{'tag':'1'}``. + For the specific URL ``/tags/1``, the ``matchdict`` generated and attached to + the :term:`request` will consist of ``{'tag':'1'}``. In this example we've again associated each of our routes with a :term:`view -callable` directly. In all cases, the request, which will have a -``matchdict`` attribute detailing the information found in the URL by the -process will be passed to the view callable. +callable` directly. In all cases, the request, which will have a ``matchdict`` +attribute detailing the information found in the URL by the process will be +passed to the view callable. Example 3 ~~~~~~~~~ -The :term:`context` resource object passed in to a view found as the result -of URL dispatch will, by default, be an instance of the object returned by -the :term:`root factory` configured at startup time (the ``root_factory`` -argument to the :term:`Configurator` used to configure the application). +The :term:`context` resource object passed in to a view found as the result of +URL dispatch will, by default, be an instance of the object returned by the +:term:`root factory` configured at startup time (the ``root_factory`` argument +to the :term:`Configurator` used to configure the application). You can override this behavior by passing in a ``factory`` argument to the :meth:`~pyramid.config.Configurator.add_route` method for a particular route. -The ``factory`` should be a callable that accepts a :term:`request` and -returns an instance of a class that will be the context resource used by the -view. +The ``factory`` should be a callable that accepts a :term:`request` and returns +an instance of a class that will be the context resource used by the view. An example of using a route with a factory: @@ -769,7 +616,7 @@ An example of using a route with a factory: :linenos: config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea') - config.add_view('myproject.views.idea_view', route_name='idea') + config.scan() The above route will manufacture an ``Idea`` resource as a :term:`context`, assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a @@ -783,11 +630,27 @@ request in its ``__init__``. For example: pass In a more complicated application, this root factory might be a class -representing a :term:`SQLAlchemy` model. +representing a :term:`SQLAlchemy` model. The view ``mypackage.views.idea_view`` +might look like this: + +.. code-block:: python + :linenos: + + @view_config(route_name='idea') + def idea_view(request): + idea = request.context + return Response(idea) + +Here, ``request.context`` is an instance of ``Idea``. If indeed the resource +object is a SQLAlchemy model, you do not even have to perform a query in the +view callable, since you have access to the resource via ``request.context``. + +See :ref:`route_factories` for more details about how to use route factories. .. index:: single: matching the root URL single: root url (matching) + pair: matching; root URL Matching the Root URL --------------------- @@ -812,24 +675,89 @@ Or provide the literal string ``/`` as the pattern: single: generating route URLs single: route URLs +.. _generating_route_urls: + Generating Route URLs --------------------- -Use the :func:`pyramid.url.route_url` function to generate URLs based on -route patterns. For example, if you've configured a route with the ``name`` +Use the :meth:`pyramid.request.Request.route_url` method to generate URLs based +on route patterns. For example, if you've configured a route with the ``name`` "foo" and the ``pattern`` "{a}/{b}/{c}", you might do this. -.. ignore-next-block .. code-block:: python :linenos: - from pyramid.url import route_url - url = route_url('foo', request, a='1', b='2', c='3') + url = request.route_url('foo', a='1', b='2', c='3') This would return something like the string ``http://example.com/1/2/3`` (at least if the current protocol and hostname implied ``http://example.com``). -See the :func:`~pyramid.url.route_url` API documentation for more -information. + +To generate only the *path* portion of a URL from a route, use the +:meth:`pyramid.request.Request.route_path` API instead of +:meth:`~pyramid.request.Request.route_url`. + +.. code-block:: python + + url = request.route_path('foo', a='1', b='2', c='3') + +This will return the string ``/1/2/3`` rather than a full URL. + +Replacement values passed to ``route_url`` or ``route_path`` must be Unicode or +bytestrings encoded in UTF-8. One exception to this rule exists: if you're +trying to replace a "remainder" match value (a ``*stararg`` replacement value), +the value may be a tuple containing Unicode strings or UTF-8 strings. + +Note that URLs and paths generated by ``route_url`` and ``route_path`` are +always URL-quoted string types (they contain no non-ASCII characters). +Therefore, if you've added a route like so: + +.. code-block:: python + + config.add_route('la', u'/La Peña/{city}') + +And you later generate a URL using ``route_path`` or ``route_url`` like so: + +.. code-block:: python + + url = request.route_path('la', city=u'Québec') + +You will wind up with the path encoded to UTF-8 and URL-quoted like so: + +.. code-block:: text + + /La%20Pe%C3%B1a/Qu%C3%A9bec + +If you have a ``*stararg`` remainder dynamic part of your route pattern: + +.. code-block:: python + + config.add_route('abc', 'a/b/c/*foo') + +And you later generate a URL using ``route_path`` or ``route_url`` using a +*string* as the replacement value: + +.. code-block:: python + + url = request.route_path('abc', foo=u'Québec/biz') + +The value you pass will be URL-quoted except for embedded slashes in the +result: + +.. code-block:: text + + /a/b/c/Qu%C3%A9bec/biz + +You can get a similar result by passing a tuple composed of path elements: + +.. code-block:: python + + url = request.route_path('abc', foo=(u'Québec', u'biz')) + +Each value in the tuple will be URL-quoted and joined by slashes in this case: + +.. code-block:: text + + /a/b/c/Qu%C3%A9bec/biz .. index:: single: static routes @@ -856,9 +784,38 @@ other non-``name`` and non-``pattern`` arguments to exception to this rule is use of the ``pregenerator`` argument, which is not ignored when ``static`` is ``True``. -.. note:: the ``static`` argument to - :meth:`~pyramid.config.Configurator.add_route` is new as of :app:`Pyramid` - 1.1. +:ref:`External routes <external_route_narr>` are implicitly static. + +.. versionadded:: 1.1 + the ``static`` argument to :meth:`~pyramid.config.Configurator.add_route`. + +.. _external_route_narr: + + +External Routes +--------------- + +.. versionadded:: 1.5 + +Route patterns that are valid URLs, are treated as external routes. Like +:ref:`static routes <static_route_narr>` they are useful for URL generation +purposes only and are never considered for matching at request time. + +.. code-block:: python + :linenos: + + >>> config = Configurator() + >>> config.add_route('youtube', 'https://youtube.com/watch/{video_id}') + ... + >>> request.route_url('youtube', video_id='oHg5SJYRHA0') + >>> "https://youtube.com/watch/oHg5SJYRHA0" + +Most pattern replacements and calls to +:meth:`pyramid.request.Request.route_url` will work as expected. However, calls +to :meth:`pyramid.request.Request.route_path` against external patterns will +raise an exception, and passing ``_app_url`` to +:meth:`~pyramid.request.Request.route_url` to generate a URL against a route +that has an external pattern will also raise an exception. .. index:: single: redirecting to slash-appended routes @@ -868,249 +825,495 @@ ignored when ``static`` is ``True``. Redirecting to Slash-Appended Routes ------------------------------------ -For behavior like Django's ``APPEND_SLASH=True``, use the -:func:`~pyramid.view.append_slash_notfound_view` view as the :term:`Not Found -view` in your application. Defining this view as the :term:`Not Found view` -is a way to automatically redirect requests where the URL lacks a trailing -slash, but requires one to match the proper route. When configured, along -with at least one other route in your application, this view will be invoked -if the value of ``PATH_INFO`` does not already end in a slash, and if the -value of ``PATH_INFO`` *plus* a slash matches any route's pattern. In this -case it does an HTTP redirect to the slash-appended ``PATH_INFO``. +For behavior like Django's ``APPEND_SLASH=True``, use the ``append_slash`` +argument to :meth:`pyramid.config.Configurator.add_notfound_view` or the +equivalent ``append_slash`` argument to the +:class:`pyramid.view.notfound_view_config` decorator. -Let's use an example, because this behavior is a bit magical. If the -``append_slash_notfound_view`` is configured in your application and your -route configuration looks like so: +Adding ``append_slash=True`` is a way to automatically redirect requests where +the URL lacks a trailing slash, but requires one to match the proper route. +When configured, along with at least one other route in your application, this +view will be invoked if the value of ``PATH_INFO`` does not already end in a +slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's +pattern. In this case it does an HTTP redirect to the slash-appended +``PATH_INFO``. In addition you may pass anything that implements +:class:`pyramid.interfaces.IResponse` which will then be used in place of the +default class (:class:`pyramid.httpexceptions.HTTPFound`). + +Let's use an example. If the following routes are configured in your +application: .. code-block:: python :linenos: - config.add_route('noslash', 'no_slash') - config.add_route('hasslash', 'has_slash/') + from pyramid.httpexceptions import HTTPNotFound + + def notfound(request): + return HTTPNotFound('Not found, bro.') + + def no_slash(request): + return Response('No slash') - config.add_view('myproject.views.no_slash', route_name='noslash') - config.add_view('myproject.views.has_slash', route_name='hasslash') + def has_slash(request): + return Response('Has slash') + + def main(g, **settings): + config = Configurator() + config.add_route('noslash', 'no_slash') + config.add_route('hasslash', 'has_slash/') + config.add_view(no_slash, route_name='noslash') + config.add_view(has_slash, route_name='hasslash') + config.add_notfound_view(notfound, append_slash=True) + +If a request enters the application with the ``PATH_INFO`` value of +``/no_slash``, the first route will match and the browser will show "No slash". +However, if a request enters the application with the ``PATH_INFO`` value of +``/no_slash/``, *no* route will match, and the slash-appending not found view +will not find a matching route with an appended slash. As a result, the +``notfound`` view will be called and it will return a "Not found, bro." body. If a request enters the application with the ``PATH_INFO`` value of ``/has_slash/``, the second route will match. If a request enters the application with the ``PATH_INFO`` value of ``/has_slash``, a route *will* be -found by the slash-appending not found view. An HTTP redirect to -``/has_slash/`` will be returned to the user's browser. +found by the slash-appending :term:`Not Found View`. An HTTP redirect to +``/has_slash/`` will be returned to the user's browser. As a result, the +``notfound`` view will never actually be called. -If a request enters the application with the ``PATH_INFO`` value of -``/no_slash``, the first route will match. However, if a request enters the -application with the ``PATH_INFO`` value of ``/no_slash/``, *no* route will -match, and the slash-appending not found view will *not* find a matching -route with an appended slash. +The following application uses the :class:`pyramid.view.notfound_view_config` +and :class:`pyramid.view.view_config` decorators and a :term:`scan` to do +exactly the same job: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import notfound_view_config, view_config + + @notfound_view_config(append_slash=True) + def notfound(request): + return HTTPNotFound('Not found, bro.') + + @view_config(route_name='noslash') + def no_slash(request): + return Response('No slash') + + @view_config(route_name='hasslash') + def has_slash(request): + return Response('Has slash') + + def main(g, **settings): + config = Configurator() + config.add_route('noslash', 'no_slash') + config.add_route('hasslash', 'has_slash/') + config.scan() .. warning:: - You **should not** rely on this mechanism to redirect ``POST`` requests. - The redirect of the slash-appending not found view will turn a ``POST`` - request into a ``GET``, losing any ``POST`` data in the original - request. + You **should not** rely on this mechanism to redirect ``POST`` requests. + The redirect of the slash-appending :term:`Not Found View` will turn a + ``POST`` request into a ``GET``, losing any ``POST`` data in the original + request. + +See :ref:`view_module` and :ref:`changing_the_notfound_view` for a more +general description of how to configure a view and/or a :term:`Not Found View`. + +.. index:: + pair: debugging; route matching + +.. _debug_routematch_section: + +Debugging Route Matching +------------------------ + +It's useful to be able to take a peek under the hood when requests that enter +your application aren't matching your routes as you expect them to. To debug +route matching, use the ``PYRAMID_DEBUG_ROUTEMATCH`` environment variable or +the ``pyramid.debug_routematch`` configuration file setting (set either to +``true``). Details of the route matching decision for a particular request to +the :app:`Pyramid` application will be printed to the ``stderr`` of the console +which you started the application from. For example: + +.. code-block:: text + :linenos: + + $ PYRAMID_DEBUG_ROUTEMATCH=true $VENV/bin/pserve development.ini + Starting server in PID 13586. + serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 + 2010-12-16 14:45:19,956 no route matched for url \ + http://localhost:6543/wontmatch + 2010-12-16 14:45:20,010 no route matched for url \ + http://localhost:6543/favicon.ico + 2010-12-16 14:41:52,084 route matched for url \ + http://localhost:6543/static/logo.png; \ + route_name: 'static/', .... + +See :ref:`environment_chapter` for more information about how and where to set +these values. + +You can also use the ``proutes`` command to see a display of all the routes +configured in your application. For more information, see +:ref:`displaying_application_routes`. + +.. _route_prefix: + +Using a Route Prefix to Compose Applications +-------------------------------------------- + +.. versionadded:: 1.2 + +The :meth:`pyramid.config.Configurator.include` method allows configuration +statements to be included from separate files. See +:ref:`building_an_extensible_app` for information about this method. Using +:meth:`pyramid.config.Configurator.include` allows you to build your +application from small and potentially reusable components. + +The :meth:`pyramid.config.Configurator.include` method accepts an argument +named ``route_prefix`` which can be useful to authors of URL-dispatch-based +applications. If ``route_prefix`` is supplied to the include method, it must +be a string. This string represents a route prefix that will be prepended to +all route patterns added by the *included* configuration. Any calls to +:meth:`pyramid.config.Configurator.add_route` within the included callable will +have their pattern prefixed with the value of ``route_prefix``. This can be +used to help mount a set of routes at a different location than the included +callable's author intended while still maintaining the same route names. For +example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator -To configure the slash-appending not found view in your application, change -the application's startup configuration, adding the following stanza: + def users_include(config): + config.add_route('show_users', '/show') + + def main(global_config, **settings): + config = Configurator() + config.include(users_include, route_prefix='/users') + +In the above configuration, the ``show_users`` route will have an effective +route pattern of ``/users/show`` instead of ``/show`` because the +``route_prefix`` argument will be prepended to the pattern. The route will +then only match if the URL path is ``/users/show``, and when the +:meth:`pyramid.request.Request.route_url` function is called with the route +name ``show_users``, it will generate a URL with that same path. + +Route prefixes are recursive, so if a callable executed via an include itself +turns around and includes another callable, the second-level route prefix will +be prepended with the first: .. code-block:: python :linenos: - config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.exceptions.NotFound') + from pyramid.config import Configurator + + def timing_include(config): + config.add_route('show_times', '/times') -See :ref:`view_module` and :ref:`changing_the_notfound_view` for more -information about the slash-appending not found view and for a more general -description of how to configure a not found view. + def users_include(config): + config.add_route('show_users', '/show') + config.include(timing_include, route_prefix='/timing') -Custom Not Found View With Slash Appended Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def main(global_config, **settings): + config = Configurator() + config.include(users_include, route_prefix='/users') -There can only be one :term:`Not Found view` in any :app:`Pyramid` -application. Even if you use :func:`~pyramid.view.append_slash_notfound_view` -as the Not Found view, :app:`Pyramid` still must generate a ``404 Not Found`` -response when it cannot redirect to a slash-appended URL; this not found -response will be visible to site users. +In the above configuration, the ``show_users`` route will still have an +effective route pattern of ``/users/show``. The ``show_times`` route, however, +will have an effective pattern of ``/users/timing/times``. -If you don't care what this 404 response looks like, and only you need -redirections to slash-appended route URLs, you may use the -:func:`~pyramid.view.append_slash_notfound_view` object as the Not Found view -as described above. However, if you wish to use a *custom* notfound view -callable when a URL cannot be redirected to a slash-appended URL, you may -wish to use an instance of the -:class:`~pyramid.view.AppendSlashNotFoundViewFactory` class as the Not Found -view, supplying a :term:`view callable` to be used as the custom notfound -view as the first argument to its constructor. For instance: +Route prefixes have no impact on the requirement that the set of route *names* +in any given Pyramid configuration must be entirely unique. If you compose +your URL dispatch application out of many small subapplications using +:meth:`pyramid.config.Configurator.include`, it's wise to use a dotted name for +your route names so they'll be unlikely to conflict with other packages that +may be added in the future. For example: .. code-block:: python - :linenos: + :linenos: - from pyramid.exceptions import NotFound - from pyramid.view import AppendSlashNotFoundViewFactory + from pyramid.config import Configurator - def notfound_view(context, request): - return HTTPNotFound('It aint there, stop trying!') + def timing_include(config): + config.add_route('timing.show_times', '/times') - custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + def users_include(config): + config.add_route('users.show_users', '/show') + config.include(timing_include, route_prefix='/timing') -The ``notfound_view`` supplied must adhere to the two-argument view callable -calling convention of ``(context, request)`` (``context`` will be the -exception object). + def main(global_config, **settings): + config = Configurator() + config.include(users_include, route_prefix='/users') -.. _cleaning_up_after_a_request: +.. index:: + single: route predicates (custom) -Cleaning Up After a Request ---------------------------- +.. _custom_route_predicates: -Sometimes it's required that some cleanup be performed at the end of a -request when a database connection is involved. +Custom Route Predicates +----------------------- + +Each of the predicate callables fed to the ``custom_predicates`` argument of +:meth:`~pyramid.config.Configurator.add_route` must be a callable accepting two +arguments. The first argument passed to a custom predicate is a dictionary +conventionally named ``info``. The second argument is the current +:term:`request` object. -For example, let's say you have a ``mypackage`` :app:`Pyramid` application -package that uses SQLAlchemy, and you'd like the current SQLAlchemy database -session to be removed after each request. Put the following in the -``mypackage.__init__`` module: +The ``info`` dictionary has a number of contained values, including ``match`` +and ``route``. ``match`` is a dictionary which represents the arguments matched +in the URL by the route. ``route`` is an object representing the route which +was matched (see :class:`pyramid.interfaces.IRoute` for the API of such a route +object). + +``info['match']`` is useful when predicates need access to the route match. +For example: -.. ignore-next-block .. code-block:: python :linenos: - from mypackage.models import DBSession + def any_of(segment_name, *allowed): + def predicate(info, request): + if info['match'][segment_name] in allowed: + return True + return predicate - from pyramid.events import subscriber - from pyramid.events import NewRequest + num_one_two_or_three = any_of('num', 'one', 'two', 'three') - def cleanup_callback(request): - DBSession.remove() + config.add_route('route_to_num', '/{num}', + custom_predicates=(num_one_two_or_three,)) - @subscriber(NewRequest) - def add_cleanup_callback(event): - event.request.add_finished_callback(cleanup_callback) +The above ``any_of`` function generates a predicate which ensures that the +match value named ``segment_name`` is in the set of allowable values +represented by ``allowed``. We use this ``any_of`` function to generate a +predicate function named ``num_one_two_or_three``, which ensures that the +``num`` segment is one of the values ``one``, ``two``, or ``three`` , and use +the result as a custom predicate by feeding it inside a tuple to the +``custom_predicates`` argument to +:meth:`~pyramid.config.Configurator.add_route`. -Registering the ``cleanup_callback`` finished callback at the start of a -request (by causing the ``add_cleanup_callback`` to receive a -:class:`pyramid.events.NewRequest` event at the start of each request) will -cause the DBSession to be removed whenever request processing has ended. -Note that in the example above, for the :class:`pyramid.events.subscriber` -decorator to "work", the :meth:`pyramid.config.Configurator.scan` method must -be called against your ``mypackage`` package during application -initialization. +A custom route predicate may also *modify* the ``match`` dictionary. For +instance, a predicate might do some type conversion of values: -.. note:: This is only an example. In particular, it is not necessary to - cause ``DBSession.remove`` to be called in an application generated from - any :app:`Pyramid` scaffold, because these all use the - ``repoze.tm2`` middleware. The cleanup done by ``DBSession.remove`` is - unnecessary when ``repoze.tm2`` middleware is in the WSGI pipeline. +.. code-block:: python + :linenos: -.. index:: - pair: URL dispatch; security + def integers(*segment_names): + def predicate(info, request): + match = info['match'] + for segment_name in segment_names: + try: + match[segment_name] = int(match[segment_name]) + except (TypeError, ValueError): + pass + return True + return predicate -.. _using_security_with_urldispatch: + ymd_to_int = integers('year', 'month', 'day') -Using :app:`Pyramid` Security With URL Dispatch --------------------------------------------------- + config.add_route('ymd', '/{year}/{month}/{day}', + custom_predicates=(ymd_to_int,)) -:app:`Pyramid` provides its own security framework which consults an -:term:`authorization policy` before allowing any application code to be -called. This framework operates in terms of an access control list, which is -stored as an ``__acl__`` attribute of a resource object. A common thing to -want to do is to attach an ``__acl__`` to the resource object dynamically for -declarative security purposes. You can use the ``factory`` argument that -points at a factory which attaches a custom ``__acl__`` to an object at its -creation time. +Note that a conversion predicate is still a predicate, so it must return +``True`` or ``False``. A predicate that does *only* conversion, such as the one +we demonstrate above, should unconditionally return ``True``. -Such a ``factory`` might look like so: +To avoid the try/except uncertainty, the route pattern can contain regular +expressions specifying requirements for that marker. For instance: + +.. code-block:: python + :linenos: + + def integers(*segment_names): + def predicate(info, request): + match = info['match'] + for segment_name in segment_names: + match[segment_name] = int(match[segment_name]) + return True + return predicate + + ymd_to_int = integers('year', 'month', 'day') + + config.add_route('ymd', '/{year:\d+}/{month:\d+}/{day:\d+}', + custom_predicates=(ymd_to_int,)) + +Now the try/except is no longer needed because the route will not match at all +unless these markers match ``\d+`` which requires them to be valid digits for +an ``int`` type conversion. + +The ``match`` dictionary passed within ``info`` to each predicate attached to a +route will be the same dictionary. Therefore, when registering a custom +predicate which modifies the ``match`` dict, the code registering the predicate +should usually arrange for the predicate to be the *last* custom predicate in +the custom predicate list. Otherwise, custom predicates which fire subsequent +to the predicate which performs the ``match`` modification will receive the +*modified* match dictionary. + +.. warning:: + + It is a poor idea to rely on ordering of custom predicates to build a + conversion pipeline, where one predicate depends on the side effect of + another. For instance, it's a poor idea to register two custom predicates, + one which handles conversion of a value to an int, the next which handles + conversion of that integer to some custom object. Just do all that in a + single custom predicate. + +The ``route`` object in the ``info`` dict is an object that has two useful +attributes: ``name`` and ``pattern``. The ``name`` attribute is the route name. +The ``pattern`` attribute is the route pattern. Here's an example of using the +route in a set of route predicates: + +.. code-block:: python + :linenos: + + def twenty_ten(info, request): + if info['route'].name in ('ymd', 'ym', 'y'): + return info['match']['year'] == '2010' + + config.add_route('y', '/{year}', custom_predicates=(twenty_ten,)) + config.add_route('ym', '/{year}/{month}', custom_predicates=(twenty_ten,)) + config.add_route('ymd', '/{year}/{month}/{day}', + custom_predicates=(twenty_ten,)) + +The above predicate, when added to a number of route configurations ensures +that the year match argument is '2010' if and only if the route name is 'ymd', +'ym', or 'y'. + +You can also caption the predicates by setting the ``__text__`` attribute. This +will help you with the ``pviews`` command (see +:ref:`displaying_application_routes`) and the ``pyramid_debugtoolbar``. + +If a predicate is a class, just add ``__text__`` property in a standard manner. .. code-block:: python :linenos: - class Article(object): - def __init__(self, request): - matchdict = request.matchdict - article = matchdict.get('article', None) - if article == '1': - self.__acl__ = [ (Allow, 'editor', 'view') ] + class DummyCustomPredicate1(object): + def __init__(self): + self.__text__ = 'my custom class predicate' -If the route ``archives/{article}`` is matched, and the article number is -``1``, :app:`Pyramid` will generate an ``Article`` :term:`context` resource -with an ACL on it that allows the ``editor`` principal the ``view`` -permission. Obviously you can do more generic things than inspect the routes -match dict to see if the ``article`` argument matches a particular string; -our sample ``Article`` factory class is not very ambitious. + class DummyCustomPredicate2(object): + __text__ = 'my custom class predicate' -.. note:: See :ref:`security_chapter` for more information about - :app:`Pyramid` security and ACLs. +If a predicate is a method, you'll need to assign it after method declaration +(see `PEP 232 <http://www.python.org/dev/peps/pep-0232/>`_). -.. _debug_routematch_section: +.. code-block:: python + :linenos: -Debugging Route Matching ------------------------- + def custom_predicate(): + pass + custom_predicate.__text__ = 'my custom method predicate' -It's useful to be able to take a peek under the hood when requests that enter -your application arent matching your routes as you expect them to. To debug -route matching, use the ``PYRAMID_DEBUG_ROUTEMATCH`` environment variable or the -``debug_routematch`` configuration file setting (set either to ``true``). -Details of the route matching decision for a particular request to the -:app:`Pyramid` application will be printed to the ``stderr`` of the console -which you started the application from. For example: +If a predicate is a classmethod, using ``@classmethod`` will not work, but you +can still easily do it by wrapping it in a classmethod call. -.. code-block:: text +.. code-block:: python :linenos: - [chrism@thinko pylonsbasic]$ PYRAMID_DEBUG_ROUTEMATCH=true \ - bin/paster serve development.ini - Starting server in PID 13586. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 - 2010-12-16 14:45:19,956 no route matched for url \ - http://localhost:6543/wontmatch - 2010-12-16 14:45:20,010 no route matched for url \ - http://localhost:6543/favicon.ico - 2010-12-16 14:41:52,084 route matched for url \ - http://localhost:6543/static/logo.png; \ - route_name: 'static/', .... + def classmethod_predicate(): + pass + classmethod_predicate.__text__ = 'my classmethod predicate' + classmethod_predicate = classmethod(classmethod_predicate) + +The same will work with ``staticmethod``, using ``staticmethod`` instead of +``classmethod``. + +.. seealso:: -See :ref:`environment_chapter` for more information about how, and where to -set these values. + See also :class:`pyramid.interfaces.IRoute` for more API documentation + about route objects. .. index:: - pair: routes; printing - single: paster proutes + single: route factory -.. _displaying_application_routes: +.. _route_factories: -Displaying All Application Routes ---------------------------------- +Route Factories +--------------- -You can use the ``paster proutes`` command in a terminal window to print a -summary of routes related to your application. Much like the ``paster -pshell`` command (see :ref:`interactive_shell`), the ``paster proutes`` -command accepts two arguments. The first argument to ``proutes`` is the path -to your application's ``.ini`` file. The second is the ``app`` section name -inside the ``.ini`` file which points to your application. +Although it is not a particularly common need in basic applications, a "route" +configuration declaration can mention a "factory". When a route matches a +request, and a factory is attached to the route, the :term:`root factory` +passed at startup time to the :term:`Configurator` is ignored. Instead the +factory associated with the route is used to generate a :term:`root` object. +This object will usually be used as the :term:`context` resource of the view +callable ultimately found via :term:`view lookup`. -For example: +.. code-block:: python + :linenos: -.. code-block:: text + config.add_route('abc', '/abc', + factory='myproject.resources.root_factory') + config.add_view('myproject.views.theview', route_name='abc') + +The factory can either be a Python object or a :term:`dotted Python name` (a +string) which points to such a Python object, as it is above. + +In this way, each route can use a different factory, making it possible to +supply a different :term:`context` resource object to the view related to each +particular route. + +A factory must be a callable which accepts a request and returns an arbitrary +Python object. For example, the below class can be used as a factory: + +.. code-block:: python :linenos: - [chrism@thinko MyProject]$ ../bin/paster proutes development.ini MyProject - Name Pattern View - ---- ------- ---- - home / <function my_view> - home2 / <function my_view> - another /another None - static/ static/*subpath <static_view object> - catchall /*subpath <function static_view> - -``paster proutes`` generates a table. The table has three columns: a Name -name column, a Pattern column, and a View column. The items listed in the -Name column are route names, the items listen in the Pattern column are route -patterns, and the items listed in the View column are representations of the -view callable that will be invoked when a request matches the associated -route pattern. The view column may show ``None`` if no associated view -callable could be found. If no routes are configured within your -application, nothing will be printed to the console when ``paster proutes`` -is executed. + class Mine(object): + def __init__(self, request): + pass + +A route factory is actually conceptually identical to the :term:`root factory` +described at :ref:`the_resource_tree`. + +Supplying a different resource factory for each route is useful when you're +trying to use a :app:`Pyramid` :term:`authorization policy` to provide +declarative, "context sensitive" security checks. Each resource can maintain a +separate :term:`ACL`, as documented in :ref:`using_security_with_urldispatch`. +It is also useful when you wish to combine URL dispatch with :term:`traversal` +as documented within :ref:`hybrid_chapter`. + +.. index:: + pair: URL dispatch; security + +.. _using_security_with_urldispatch: + +Using :app:`Pyramid` Security with URL Dispatch +----------------------------------------------- + +:app:`Pyramid` provides its own security framework which consults an +:term:`authorization policy` before allowing any application code to be called. +This framework operates in terms of an access control list, which is stored as +an ``__acl__`` attribute of a resource object. A common thing to want to do is +to attach an ``__acl__`` to the resource object dynamically for declarative +security purposes. You can use the ``factory`` argument that points at a +factory which attaches a custom ``__acl__`` to an object at its creation time. + +Such a ``factory`` might look like so: + +.. code-block:: python + :linenos: + + class Article(object): + def __init__(self, request): + matchdict = request.matchdict + article = matchdict.get('article', None) + if article == '1': + self.__acl__ = [ (Allow, 'editor', 'view') ] + +If the route ``archives/{article}`` is matched, and the article number is +``1``, :app:`Pyramid` will generate an ``Article`` :term:`context` resource +with an ACL on it that allows the ``editor`` principal the ``view`` permission. +Obviously you can do more generic things than inspect the route's match dict to +see if the ``article`` argument matches a particular string. Our sample +``Article`` factory class is not very ambitious. + +.. note:: + + See :ref:`security_chapter` for more information about :app:`Pyramid` + security and ACLs. + +.. index:: + pair: route; view callable lookup details Route View Callable Registration and Lookup Details --------------------------------------------------- @@ -1119,10 +1322,9 @@ When a request enters the system which matches the pattern of the route, the usual result is simple: the view callable associated with the route is invoked with the request that caused the invocation. -For most usage, you needn't understand more than this; how it works is an -implementation detail. In the interest of completeness, however, we'll -explain how it *does* work in the this section. You can skip it if you're -uninterested. +For most usage, you needn't understand more than this. How it works is an +implementation detail. In the interest of completeness, however, we'll explain +how it *does* work in this section. You can skip it if you're uninterested. When a view is associated with a route configuration, :app:`Pyramid` ensures that a :term:`view configuration` is registered that will always be found @@ -1138,25 +1340,28 @@ when the route pattern is matched during a request. To do so: - At runtime, when a request causes any route to match, the :term:`request` object is decorated with the route-specific interface. -- The fact that the request is decorated with a route-specific interface - causes the view lookup machinery to always use the view callable registered +- The fact that the request is decorated with a route-specific interface causes + the :term:`view lookup` machinery to always use the view callable registered using that interface by the route configuration to service requests that match the route pattern. -In this way, we supply a shortcut to the developer. Under the hood, the -:term:`resource location` and :term:`view lookup` subsystems provided by -:app:`Pyramid` are still being utilized, but in a way which does not require -a developer to understand either of them in detail. It also means that we -can allow a developer to combine :term:`URL dispatch` and :term:`traversal` -in various exceptional cases as documented in :ref:`hybrid_chapter`. - -To gain a better understanding of how routes and views are associated in a -real application, you can use the ``paster pviews`` command, as documented -in :ref:`displaying_matching_views`. +As we can see from the above description, technically, URL dispatch doesn't +actually map a URL pattern directly to a view callable. Instead URL dispatch +is a :term:`resource location` mechanism. A :app:`Pyramid` :term:`resource +location` subsystem (i.e., :term:`URL dispatch` or :term:`traversal`) finds a +:term:`resource` object that is the :term:`context` of a :term:`request`. Once +the :term:`context` is determined, a separate subsystem named :term:`view +lookup` is then responsible for finding and invoking a :term:`view callable` +based on information available in the context and the request. When URL +dispatch is used, the resource location and view lookup subsystems provided by +:app:`Pyramid` are still being utilized, but in a way which does not require a +developer to understand either of them in detail. + +If no route is matched using :term:`URL dispatch`, :app:`Pyramid` falls back to +:term:`traversal` to handle the :term:`request`. References ---------- A tutorial showing how :term:`URL dispatch` can be used to create a :app:`Pyramid` application exists in :ref:`bfg_sql_wiki_tutorial`. - diff --git a/docs/narr/vhosting.rst b/docs/narr/vhosting.rst index d3ff260e3..0edf03353 100644 --- a/docs/narr/vhosting.rst +++ b/docs/narr/vhosting.rst @@ -6,66 +6,66 @@ Virtual Hosting =============== -"Virtual hosting" is, loosely, the act of serving a :app:`Pyramid` -application or a portion of a :app:`Pyramid` application under a -URL space that it does not "naturally" inhabit. +"Virtual hosting" is, loosely, the act of serving a :app:`Pyramid` application +or a portion of a :app:`Pyramid` application under a URL space that it does not +"naturally" inhabit. -:app:`Pyramid` provides facilities for serving an application under -a URL "prefix", as well as serving a *portion* of a :term:`traversal` -based application under a root URL. +:app:`Pyramid` provides facilities for serving an application under a URL +"prefix", as well as serving a *portion* of a :term:`traversal` based +application under a root URL. + +.. index:: + single: hosting an app under a prefix Hosting an Application Under a URL Prefix ----------------------------------------- -:app:`Pyramid` supports a common form of virtual hosting whereby you -can host a :app:`Pyramid` application as a "subset" of some other site -(e.g. under ``http://example.com/mypyramidapplication/`` as opposed to -under ``http://example.com/``). +:app:`Pyramid` supports a common form of virtual hosting whereby you can host a +:app:`Pyramid` application as a "subset" of some other site (e.g., under +``http://example.com/mypyramidapplication/`` as opposed to under +``http://example.com/``). -If you use a "pure Python" environment, this functionality is provided -by Paste's `urlmap <http://pythonpaste.org/modules/urlmap.html>`_ -"composite" WSGI application. Alternately, you can use -:term:`mod_wsgi` to serve your application, which handles this virtual -hosting translation for you "under the hood". +If you use a "pure Python" environment, this functionality can be provided by +Paste's `urlmap <http://pythonpaste.org/modules/urlmap.html>`_ "composite" WSGI +application. Alternatively, you can use :term:`mod_wsgi` to serve your +application, which handles this virtual hosting translation for you "under the +hood". -If you use the ``urlmap`` composite application "in front" of a -:app:`Pyramid` application or if you use :term:`mod_wsgi` to serve -up a :app:`Pyramid` application, nothing special needs to be done -within the application for URLs to be generated that contain a -prefix. :mod:`paste.urlmap` and :term:`mod_wsgi` manipulate the -:term:`WSGI` environment in such a way that the ``PATH_INFO`` and -``SCRIPT_NAME`` variables are correct for some given prefix. +If you use the ``urlmap`` composite application "in front" of a :app:`Pyramid` +application or if you use :term:`mod_wsgi` to serve up a :app:`Pyramid` +application, nothing special needs to be done within the application for URLs +to be generated that contain a prefix. :mod:`paste.urlmap` and :term:`mod_wsgi` +manipulate the :term:`WSGI` environment in such a way that the ``PATH_INFO`` +and ``SCRIPT_NAME`` variables are correct for some given prefix. -Here's an example of a PasteDeploy configuration snippet that includes -a ``urlmap`` composite. +Here's an example of a PasteDeploy configuration snippet that includes a +``urlmap`` composite. .. code-block:: ini :linenos: [app:mypyramidapp] - use = egg:mypyramidapp#app + use = egg:mypyramidapp [composite:main] use = egg:Paste#urlmap /pyramidapp = mypyramidapp -This "roots" the :app:`Pyramid` application at the prefix -``/pyramidapp`` and serves up the composite as the "main" application -in the file. +This "roots" the :app:`Pyramid` application at the prefix ``/pyramidapp`` and +serves up the composite as the "main" application in the file. -.. note:: If you're using an Apache server to proxy to a Paste - ``urlmap`` composite, you may have to use the `ProxyPreserveHost +.. note:: If you're using an Apache server to proxy to a Paste ``urlmap`` + composite, you may have to use the `ProxyPreserveHost <http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypreservehost>`_ directive to pass the original ``HTTP_HOST`` header along to the - application, so URLs get generated properly. As of this writing - the ``urlmap`` composite does not seem to respect the - ``HTTP_X_FORWARDED_HOST`` parameter, which will contain the - original host header even if ``HTTP_HOST`` is incorrect. + application, so URLs get generated properly. As of this writing the + ``urlmap`` composite does not seem to respect the ``HTTP_X_FORWARDED_HOST`` + parameter, which will contain the original host header even if ``HTTP_HOST`` + is incorrect. -If you use :term:`mod_wsgi`, you do not need to use a ``composite`` -application in your ``.ini`` file. The ``WSGIScriptAlias`` -configuration setting in a :term:`mod_wsgi` configuration does the -work for you: +If you use :term:`mod_wsgi`, you do not need to use a ``composite`` application +in your ``.ini`` file. The ``WSGIScriptAlias`` configuration setting in a +:term:`mod_wsgi` configuration does the work for you: .. code-block:: apache :linenos: @@ -84,8 +84,7 @@ Virtual Root Support -------------------- :app:`Pyramid` also supports "virtual roots", which can be used in -:term:`traversal` -based (but not :term:`URL dispatch` -based) -applications. +:term:`traversal`-based (but not :term:`URL dispatch`-based) applications. Virtual root support is useful when you'd like to host some resource in a :app:`Pyramid` resource tree as an application under a URL pathname that does @@ -95,18 +94,17 @@ object at the traversal path ``/cms`` as an application reachable via To specify a virtual root, cause an environment variable to be inserted into the WSGI environ named ``HTTP_X_VHM_ROOT`` with a value that is the absolute -pathname to the resource object in the resource tree that should behave as -the "root" resource. As a result, the traversal machinery will respect this -value during traversal (prepending it to the PATH_INFO before traversal -starts), and the :func:`pyramid.url.resource_url` API will generate the +pathname to the resource object in the resource tree that should behave as the +"root" resource. As a result, the traversal machinery will respect this value +during traversal (prepending it to the PATH_INFO before traversal starts), and +the :meth:`pyramid.request.Request.resource_url` API will generate the "correct" virtually-rooted URLs. -An example of an Apache ``mod_proxy`` configuration that will host the -``/cms`` subobject as ``http://www.example.com/`` using this facility -is below: +An example of an Apache ``mod_proxy`` configuration that will host the ``/cms`` +subobject as ``http://www.example.com/`` using this facility is below: .. code-block:: apache - :linenos: + :linenos: NameVirtualHost *:80 @@ -118,32 +116,30 @@ is below: RequestHeader add X-Vhm-Root /cms </VirtualHost> -.. note:: Use of the ``RequestHeader`` directive requires that the - Apache `mod_headers - <http://httpd.apache.org/docs/2.2/mod/mod_headers.html>`_ module be - available in the Apache environment you're using. +.. note:: Use of the ``RequestHeader`` directive requires that the Apache + `mod_headers <http://httpd.apache.org/docs/2.2/mod/mod_headers.html>`_ + module be available in the Apache environment you're using. -For a :app:`Pyramid` application running under :term:`mod_wsgi`, -the same can be achieved using ``SetEnv``: +For a :app:`Pyramid` application running under :term:`mod_wsgi`, the same can +be achieved using ``SetEnv``: .. code-block:: apache - :linenos: + :linenos: <Location /> SetEnv HTTP_X_VHM_ROOT /cms </Location> -Setting a virtual root has no effect when using an application based -on :term:`URL dispatch`. +Setting a virtual root has no effect when using an application based on +:term:`URL dispatch`. Further Documentation and Examples ---------------------------------- The API documentation in :ref:`traversal_module` documents a -:func:`pyramid.traversal.virtual_root` API. When called, it -returns the virtual root object (or the physical root object if no -virtual root has been specified). - -:ref:`modwsgi_tutorial` has detailed information about using -:term:`mod_wsgi` to serve :app:`Pyramid` applications. +:func:`pyramid.traversal.virtual_root` API. When called, it returns the +virtual root object (or the physical root object if no virtual root has been +specified). +:ref:`modwsgi_tutorial` has detailed information about using :term:`mod_wsgi` +to serve :app:`Pyramid` applications. diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 5640800a2..cd5b8feb0 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -2,38 +2,27 @@ .. _view_configuration: +.. _view_lookup: + View Configuration ================== .. index:: single: view lookup -:term:`View configuration` controls how :term:`view lookup` operates in -your application. In earlier chapters, you have been exposed to a few -simple view configuration declarations without much explanation. In this -chapter we will explore the subject in detail. - -.. _view_lookup: - -View Lookup and Invocation --------------------------- - -:term:`View lookup` is the :app:`Pyramid` subsystem responsible for finding -an invoking a :term:`view callable`. The view lookup subsystem is passed a -:term:`context` and a :term:`request` object. +:term:`View lookup` is the :app:`Pyramid` subsystem responsible for finding and +invoking a :term:`view callable`. :term:`View configuration` controls how +:term:`view lookup` operates in your application. During any given request, +view configuration information is compared against request data by the view +lookup subsystem in order to find the "best" view callable for that request. -:term:`View configuration` information stored within in the -:term:`application registry` is compared against the context and request by -the view lookup subsystem in order to find the "best" view callable for the -set of circumstances implied by the context and request. +In earlier chapters, you have been exposed to a few simple view configuration +declarations without much explanation. In this chapter we will explore the +subject in detail. -:term:`View predicate` attributes are an important part of view -configuration that enables the :term:`View lookup` subsystem to find and -invoke the appropriate view. Predicate attributes can be thought of -like "narrowers". In general, the greater number of predicate -attributes possessed by a view's configuration, the more specific the -circumstances need to be before the registered view callable will be -invoked. +.. index:: + pair: resource; mapping to view callable + pair: URL pattern; mapping to view callable Mapping a Resource or URL Pattern to a View Callable ---------------------------------------------------- @@ -41,31 +30,23 @@ Mapping a Resource or URL Pattern to a View Callable A developer makes a :term:`view callable` available for use within a :app:`Pyramid` application via :term:`view configuration`. A view configuration associates a view callable with a set of statements that -determine the set of circumstances which must be true for the view callable -to be invoked. +determine the set of circumstances which must be true for the view callable to +be invoked. A view configuration statement is made about information present in the :term:`context` resource and the :term:`request`. -View configuration is performed in one of these ways: +View configuration is performed in one of two ways: -- by running a :term:`scan` against application source code which has a +- By running a :term:`scan` against application source code which has a :class:`pyramid.view.view_config` decorator attached to a Python object as per :ref:`mapping_views_using_a_decorator_section`. -- by using the :meth:`pyramid.config.Configurator.add_view` method as per +- By using the :meth:`pyramid.config.Configurator.add_view` method as per :ref:`mapping_views_using_imperative_config_section`. -- By specifying a view within a :term:`route configuration`. View - configuration via a route configuration is performed by using the - :meth:`pyramid.config.Configurator.add_route` method, passing a ``view`` - argument specifying a view callable. This pattern of view configuration is - deprecated as of :app:`Pyramid` 1.1. - -.. note:: A package named ``pyramid_handlers`` (available from PyPI) provides - an analogue of :term:`Pylons` -style "controllers", which are a special - kind of view class which provides more automation when your application - uses :term:`URL dispatch` solely. +.. index:: + single: view configuration parameters .. _view_configuration_parameters: @@ -75,90 +56,171 @@ View Configuration Parameters All forms of view configuration accept the same general types of arguments. Many arguments supplied during view configuration are :term:`view predicate` -arguments. View predicate arguments used during view configuration are used -to narrow the set of circumstances in which :term:`view lookup` will find a +arguments. View predicate arguments used during view configuration are used to +narrow the set of circumstances in which :term:`view lookup` will find a particular view callable. -In general, the fewer number of predicates which are supplied to a -particular view configuration, the more likely it is that the associated -view callable will be invoked. The greater the number supplied, the -less likely. A view with five predicates will always be found and -evaluated before a view with two, for example. All predicates must -match for the associated view to be called. - -This does not mean however, that :app:`Pyramid` "stops looking" when it -finds a view registration with predicates that don't match. If one set -of view predicates does not match, the "next most specific" view (if -any) is consulted for predicates, and so on, until a view is found, or -no view can be matched up with the request. The first view with a set -of predicates all of which match the request environment will be -invoked. +:term:`View predicate` attributes are an important part of view configuration +that enables the :term:`view lookup` subsystem to find and invoke the +appropriate view. The greater the number of predicate attributes possessed by +a view's configuration, the more specific the circumstances need to be before +the registered view callable will be invoked. The fewer the number of +predicates which are supplied to a particular view configuration, the more +likely it is that the associated view callable will be invoked. A view with +five predicates will always be found and evaluated before a view with two, for +example. + +This does not mean however, that :app:`Pyramid` "stops looking" when it finds a +view registration with predicates that don't match. If one set of view +predicates does not match, the "next most specific" view (if any) is consulted +for predicates, and so on, until a view is found, or no view can be matched up +with the request. The first view with a set of predicates all of which match +the request environment will be invoked. If no view can be found with predicates which allow it to be matched up with the request, :app:`Pyramid` will return an error to the user's browser, representing a "not found" (404) page. See :ref:`changing_the_notfound_view` -for more information about changing the default notfound view. +for more information about changing the default :term:`Not Found View`. -Some view configuration arguments are non-predicate arguments. These tend to +Other view configuration arguments are non-predicate arguments. These tend to modify the response of the view callable or prevent the view callable from being invoked due to an authorization policy. The presence of non-predicate arguments in a view configuration does not narrow the circumstances in which the view callable will be invoked. +.. _nonpredicate_view_args: + Non-Predicate Arguments +++++++++++++++++++++++ ``permission`` The name of a :term:`permission` that the user must possess in order to - invoke the :term:`view callable`. See :ref:`view_security_section` for - more information about view security and permissions. + invoke the :term:`view callable`. See :ref:`view_security_section` for more + information about view security and permissions. - If ``permission`` is not supplied, no permission is registered for this - view (it's accessible by any caller). + If ``permission`` is not supplied, no permission is registered for this view + (it's accessible by any caller). ``attr`` The view machinery defaults to using the ``__call__`` method of the :term:`view callable` (or the function itself, if the view callable is a function) to obtain a response. The ``attr`` value allows you to vary the - method attribute used to obtain the response. For example, if your view - was a class, and the class has a method named ``index`` and you wanted to - use this method instead of the class' ``__call__`` method to return the - response, you'd say ``attr="index"`` in the view configuration for the - view. This is most useful when the view definition is a class. + method attribute used to obtain the response. For example, if your view was + a class, and the class has a method named ``index`` and you wanted to use + this method instead of the class's ``__call__`` method to return the + response, you'd say ``attr="index"`` in the view configuration for the view. + This is most useful when the view definition is a class. If ``attr`` is not supplied, ``None`` is used (implying the function itself - if the view is a function, or the ``__call__`` callable attribute if the - view is a class). + if the view is a function, or the ``__call__`` callable attribute if the view + is a class). ``renderer`` - Denotes the :term:`renderer` implementation which will be used to construct - a :term:`response` from the associated view callable's return value. (see - also :ref:`renderers_chapter`). - - This is either a single string term (e.g. ``json``) or a string implying a - path or :term:`asset specification` (e.g. ``templates/views.pt``) naming a - :term:`renderer` implementation. If the ``renderer`` value does not - contain a dot (``.``), the specified string will be used to look up a - renderer implementation, and that renderer implementation will be used to - construct a response from the view return value. If the ``renderer`` value - contains a dot (``.``), the specified term will be treated as a path, and - the filename extension of the last element in the path will be used to look - up the renderer implementation, which will be passed the full path. - - When the renderer is a path, although a path is usually just a simple - relative pathname (e.g. ``templates/foo.pt``, implying that a template - named "foo.pt" is in the "templates" directory relative to the directory of - the current :term:`package`), a path can be absolute, starting with a slash - on UNIX or a drive letter prefix on Windows. The path can alternately be a - :term:`asset specification` in the form - ``some.dotted.package_name:relative/path``, making it possible to address - template assets which live in a separate package. + Denotes the :term:`renderer` implementation which will be used to construct a + :term:`response` from the associated view callable's return value. + + .. seealso:: See also :ref:`renderers_chapter`. + + This is either a single string term (e.g., ``json``) or a string implying a + path or :term:`asset specification` (e.g., ``templates/views.pt``) naming a + :term:`renderer` implementation. If the ``renderer`` value does not contain + a dot (``.``), the specified string will be used to look up a renderer + implementation, and that renderer implementation will be used to construct a + response from the view return value. If the ``renderer`` value contains a + dot (``.``), the specified term will be treated as a path, and the filename + extension of the last element in the path will be used to look up the + renderer implementation, which will be passed the full path. + + When the renderer is a path—although a path is usually just a simple relative + pathname (e.g., ``templates/foo.pt``, implying that a template named "foo.pt" + is in the "templates" directory relative to the directory of the current + :term:`package`)—the path can be absolute, starting with a slash on UNIX or a + drive letter prefix on Windows. The path can alternatively be a :term:`asset + specification` in the form ``some.dotted.package_name:relative/path``, making + it possible to address template assets which live in a separate package. The ``renderer`` attribute is optional. If it is not defined, the "null" renderer is assumed (no rendering is performed and the value is passed back - to the upstream :app:`Pyramid` machinery unchanged). Note that if the - view callable itself returns a :term:`response` (see :ref:`the_response`), - the specified renderer implementation is never called. + to the upstream :app:`Pyramid` machinery unchanged). Note that if the view + callable itself returns a :term:`response` (see :ref:`the_response`), the + specified renderer implementation is never called. + +``http_cache`` + When you supply an ``http_cache`` value to a view configuration, the + ``Expires`` and ``Cache-Control`` headers of a response generated by the + associated view callable are modified. The value for ``http_cache`` may be + one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a number of + seconds. This number of seconds will be used to compute the ``Expires`` + header and the ``Cache-Control: max-age`` parameter of responses to + requests which call this view. For example: ``http_cache=3600`` instructs + the requesting browser to 'cache this response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a ``datetime.timedelta`` + instance, it will be converted into a number of seconds, and that number of + seconds will be used to compute the ``Expires`` header and the + ``Cache-Control: max-age`` parameter of responses to requests which call + this view. For example: ``http_cache=datetime.timedelta(days=1)`` + instructs the requesting browser to 'cache this response for a day, + please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and ``Expires`` + headers present in all responses from this view will be composed such that + client browser cache (and any intermediate caches) are instructed to never + cache the response. + + - A two-tuple. If it's a two-tuple (e.g., ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a nonzero integer + or a ``datetime.timedelta`` instance. In either case this value will be + used as the number of seconds to cache the response. The second value in + the tuple must be a dictionary. The values present in the dictionary will + be used as input to the ``Cache-Control`` response header. For example: + ``http_cache=(3600, {'public':True})`` means 'cache for an hour, and add + ``public`` to the Cache-Control header of the response'. All keys and + values supported by the ``webob.cachecontrol.CacheControl`` interface may + be added to the dictionary. Supplying ``{'public':True}`` is equivalent to + calling ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to calling + ``response.cache_expires(value[0], **value[1])`` within your view's body. + + If you wish to avoid influencing the ``Expires`` header, and instead wish to + only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` with + the first element of ``None``, i.e., ``(None, {'public':True})``. + + +``require_csrf`` + + CSRF checks will affect any request method that is not defined as a "safe" + method by RFC2616. In pratice this means that GET, HEAD, OPTIONS, and TRACE + methods will pass untouched and all others methods will require CSRF. This + option is used in combination with the ``pyramid.require_default_csrf`` + setting to control which request parameters are checked for CSRF tokens. + + This feature requires a configured :term:`session factory`. + + If this option is set to ``True`` then CSRF checks will be enabled for POST + requests to this view. The required token will be whatever was specified by + the ``pyramid.require_default_csrf`` setting, or will fallback to + ``csrf_token``. + + If this option is set to a string then CSRF checks will be enabled and it + will be used as the required token regardless of the + ``pyramid.require_default_csrf`` setting. + + If this option is set to ``False`` then CSRF checks will be disabled + regardless of the ``pyramid.require_default_csrf`` setting. + + In addition, if this option is set to ``True`` or a string then CSRF origin + checking will be enabled. + + See :ref:`auto_csrf_checking` for more information. + + .. versionadded:: 1.7 ``wrapper`` The :term:`view name` of a different :term:`view configuration` which will @@ -167,52 +229,82 @@ Non-Predicate Arguments this view as the ``request.wrapped_response`` attribute of its own request. Using a wrapper makes it possible to "chain" views together to form a composite response. The response of the outermost wrapper view will be - returned to the user. The wrapper view will be found as any view is found: - see :ref:`view_lookup`. The "best" wrapper view will be found based on the - lookup ordering: "under the hood" this wrapper view is looked up via + returned to the user. The wrapper view will be found as any view is found. + See :ref:`view_lookup`. The "best" wrapper view will be found based on the + lookup ordering. "Under the hood" this wrapper view is looked up via ``pyramid.view.render_view_to_response(context, request, - 'wrapper_viewname')``. The context and request of a wrapper view is the - same context and request of the inner view. + 'wrapper_viewname')``. The context and request of a wrapper view is the same + context and request of the inner view. If ``wrapper`` is not supplied, no wrapper view is used. ``decorator`` A :term:`dotted Python name` to a function (or the function itself) which - will be used to decorate the registered :term:`view callable`. The - decorator function will be called with the view callable as a single - argument. The view callable it is passed will accept ``(context, - request)``. The decorator must return a replacement view callable which - also accepts ``(context, request)``. + will be used to decorate the registered :term:`view callable`. The decorator + function will be called with the view callable as a single argument. The + view callable it is passed will accept ``(context, request)``. The decorator + must return a replacement view callable which also accepts ``(context, + request)``. The ``decorator`` may also be an iterable of decorators, in which + case they will be applied one after the other to the view, in reverse order. + For example:: + + @view_config(..., decorator=(decorator2, decorator1)) + def myview(request): + ... + + Is similar to doing:: + + @view_config(...) + @decorator2 + @decorator1 + def myview(request): + ... + + All view callables in the decorator chain must return a response object + implementing :class:`pyramid.interfaces.IResponse` or raise an exception: + + .. code-block:: python + + def log_timer(wrapped): + def wrapper(context, request): + start = time.time() + response = wrapped(context, request) + duration = time.time() - start + response.headers['X-View-Time'] = '%.3f' % (duration,) + log.info('view took %.3f seconds', duration) + return response + return wrapper ``mapper`` A Python object or :term:`dotted Python name` which refers to a :term:`view mapper`, or ``None``. By default it is ``None``, which indicates that the view should use the default view mapper. This plug-point is useful for - Pyramid extension developers, but it's not very useful for 'civilians' who + Pyramid extension developers, but it's not very useful for "civilians" who are just developing stock Pyramid applications. Pay no attention to the man behind the curtain. Predicate Arguments +++++++++++++++++++ -These arguments modify view lookup behavior. In general, the more predicate -arguments that are supplied, the more specific, and narrower the usage of the +These arguments modify view lookup behavior. In general the more predicate +arguments that are supplied, the more specific and narrower the usage of the configured view. ``name`` - The :term:`view name` required to match this view callable. Read - :ref:`traversal_chapter` to understand the concept of a view name. + The :term:`view name` required to match this view callable. A ``name`` + argument is typically only used when your application uses :term:`traversal`. + Read :ref:`traversal_chapter` to understand the concept of a view name. If ``name`` is not supplied, the empty string is used (implying the default view). ``context`` - An object representing a Python class that the :term:`context` resource - must be an instance of *or* the :term:`interface` that the :term:`context` + An object representing a Python class of which the :term:`context` resource + must be an instance *or* the :term:`interface` that the :term:`context` resource must provide in order for this view to be found and called. This predicate is true when the :term:`context` resource is an instance of the - represented class or if the :term:`context` resource provides the - represented interface; it is otherwise false. + represented class or if the :term:`context` resource provides the represented + interface; it is otherwise false. If ``context`` is not supplied, the value ``None``, which matches any resource, is used. @@ -222,87 +314,109 @@ configured view. the named route has matched. This value must match the ``name`` of a :term:`route configuration` - declaration (see :ref:`urldispatch_chapter`) that must match before this - view will be called. Note that the ``route`` configuration referred to by + declaration (see :ref:`urldispatch_chapter`) that must match before this view + will be called. Note that the ``route`` configuration referred to by ``route_name`` will usually have a ``*traverse`` token in the value of its ``pattern``, representing a part of the path that will be used by :term:`traversal` against the result of the route's :term:`root factory`. If ``route_name`` is not supplied, the view callable will only have a chance of being invoked if no other route was matched. This is when the - request/context pair found via :term:`resource location` does not indicate - it matched any configured route. + request/context pair found via :term:`resource location` does not indicate it + matched any configured route. ``request_type`` This value should be an :term:`interface` that the :term:`request` must provide in order for this view to be found and called. - If ``request_type`` is not supplied, the value ``None`` is used, implying - any request type. + If ``request_type`` is not supplied, the value ``None`` is used, implying any + request type. *This is an advanced feature, not often used by "civilians"*. ``request_method`` - This value can be one of the strings ``GET``, ``POST``, ``PUT``, - ``DELETE``, or ``HEAD`` representing an HTTP ``REQUEST_METHOD``. A view - declaration with this argument ensures that the view will only be called - when the request's ``method`` attribute (aka the ``REQUEST_METHOD`` of the - WSGI environment) string matches the supplied value. + This value can be either a string (such as ``"GET"``, ``"POST"``, + ``"PUT"``, ``"DELETE"``, ``"HEAD"``, or ``"OPTIONS"``) representing an HTTP + ``REQUEST_METHOD`` or a tuple containing one or more of these strings. A + view declaration with this argument ensures that the view will only be called + when the ``method`` attribute of the request (i.e., the ``REQUEST_METHOD`` of + the WSGI environment) matches a supplied value. - If ``request_method`` is not supplied, the view will be invoked regardless - of the ``REQUEST_METHOD`` of the :term:`WSGI` environment. + .. versionchanged:: 1.4 + The use of ``"GET"`` also implies that the view will respond to ``"HEAD"``. + + If ``request_method`` is not supplied, the view will be invoked regardless of + the ``REQUEST_METHOD`` of the :term:`WSGI` environment. ``request_param`` - This value can be any string. A view declaration with this argument - ensures that the view will only be called when the :term:`request` has a - key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` - variable) that has a name which matches the supplied value. - - If the value supplied has a ``=`` sign in it, - e.g. ``request_param="foo=123"``, then the key (``foo``) must both exist - in the ``request.params`` dictionary, *and* the value must match the right - hand side of the expression (``123``) for the view to "match" the current - request. + This value can be any string or a sequence of strings. A view declaration + with this argument ensures that the view will only be called when the + :term:`request` has a key in the ``request.params`` dictionary (an HTTP + ``GET`` or ``POST`` variable) that has a name which matches the supplied + value. + + If any value supplied has an ``=`` sign in it, e.g., + ``request_param="foo=123"``, then the key (``foo``) must both exist in the + ``request.params`` dictionary, *and* the value must match the right hand side + of the expression (``123``) for the view to "match" the current request. If ``request_param`` is not supplied, the view will be invoked without consideration of keys and values in the ``request.params`` dictionary. +``match_param`` + This param may be either a single string of the format "key=value" or a tuple + containing one or more of these strings. + + This argument ensures that the view will only be called when the + :term:`request` has key/value pairs in its :term:`matchdict` that equal those + supplied in the predicate. For example, ``match_param="action=edit"`` would + require the ``action`` parameter in the :term:`matchdict` match the right + hand side of the expression (``edit``) for the view to "match" the current + request. + + If the ``match_param`` is a tuple, every key/value pair must match for the + predicate to pass. + + If ``match_param`` is not supplied, the view will be invoked without + consideration of the keys and values in ``request.matchdict``. + + .. versionadded:: 1.2 + ``containment`` - This value should be a reference to a Python class or :term:`interface` - that a parent object in the context resource's :term:`lineage` must provide - in order for this view to be found and called. The resources in your - resource tree must be "location-aware" to use this feature. + This value should be a reference to a Python class or :term:`interface` that + a parent object in the context resource's :term:`lineage` must provide in + order for this view to be found and called. The resources in your resource + tree must be "location-aware" to use this feature. - If ``containment`` is not supplied, the interfaces and classes in the - lineage are not considered when deciding whether or not to invoke the view - callable. + If ``containment`` is not supplied, the interfaces and classes in the lineage + are not considered when deciding whether or not to invoke the view callable. See :ref:`location_aware` for more information about location-awareness. ``xhr`` This value should be either ``True`` or ``False``. If this value is specified and is ``True``, the :term:`WSGI` environment must possess an - ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header that has the + ``HTTP_X_REQUESTED_WITH`` header (i.e., ``X-Requested-With``) that has the value ``XMLHttpRequest`` for the associated view callable to be found and called. This is useful for detecting AJAX requests issued from jQuery, - Prototype and other Javascript libraries. + Prototype, and other Javascript libraries. - If ``xhr`` is not specified, the ``HTTP_X_REQUESTED_WITH`` HTTP header is - not taken into consideration when deciding whether or not to invoke the + If ``xhr`` is not specified, the ``HTTP_X_REQUESTED_WITH`` HTTP header is not + taken into consideration when deciding whether or not to invoke the associated view callable. ``accept`` - The value of this argument represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this value is - specified, it must be in one of the following forms: a mimetype match token - in the form ``text/plain``, a wildcard mimetype match token in the form - ``text/*`` or a match-all wildcard mimetype match token in the form - ``*/*``. If any of the forms matches the ``Accept`` header of the request, - this predicate will be true. - - If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not - taken into consideration when deciding whether or not to invoke the - associated view callable. + The value of this argument represents a match query for one or more mimetypes + in the ``Accept`` HTTP request header. If this value is specified, it must + be in one of the following forms: a mimetype match token in the form + ``text/plain``, a wildcard mimetype match token in the form ``text/*``, or a + match-all wildcard mimetype match token in the form ``*/*``. If any of the + forms matches the ``Accept`` header of the request, this predicate will be + true. + + If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not taken + into consideration when deciding whether or not to invoke the associated view + callable. ``header`` This value represents an HTTP header name or a header name/value pair. @@ -310,77 +424,155 @@ configured view. If ``header`` is specified, it must be a header name or a ``headername:headervalue`` pair. - If ``header`` is specified without a value (a bare header name only, - e.g. ``If-Modified-Since``), the view will only be invoked if the HTTP - header exists with any value in the request. + If ``header`` is specified without a value (a bare header name only, e.g., + ``If-Modified-Since``), the view will only be invoked if the HTTP header + exists with any value in the request. - If ``header`` is specified, and possesses a name/value pair - (e.g. ``User-Agent:Mozilla/.*``), the view will only be invoked if the HTTP - header exists *and* the HTTP header matches the value requested. When the - ``headervalue`` contains a ``:`` (colon), it will be considered a - name/value pair (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). - The value portion should be a regular expression. + If ``header`` is specified, and possesses a name/value pair (e.g., + ``User-Agent:Mozilla/.*``), the view will only be invoked if the HTTP header + exists *and* the HTTP header matches the value requested. When the + ``headervalue`` contains a ``:`` (colon), it will be considered a name/value + pair (e.g., ``User-Agent:Mozilla/.*`` or ``Host:localhost``). The value + portion should be a regular expression. Whether or not the value represents a header name or a header name/value pair, the case of the header name is not significant. - If ``header`` is not specified, the composition, presence or absence of - HTTP headers is not taken into consideration when deciding whether or not - to invoke the associated view callable. + If ``header`` is not specified, the composition, presence, or absence of HTTP + headers is not taken into consideration when deciding whether or not to + invoke the associated view callable. ``path_info`` This value represents a regular expression pattern that will be tested - against the ``PATH_INFO`` WSGI environment variable to decide whether or - not to call the associated view callable. If the regex matches, this - predicate will be ``True``. + against the ``PATH_INFO`` WSGI environment variable to decide whether or not + to call the associated view callable. If the regex matches, this predicate + will be ``True``. If ``path_info`` is not specified, the WSGI ``PATH_INFO`` is not taken into consideration when deciding whether or not to invoke the associated view callable. +``check_csrf`` + If specified, this value should be one of ``None``, ``True``, ``False``, or a + string representing the "check name". If the value is ``True`` or a string, + CSRF checking will be performed. If the value is ``False`` or ``None``, CSRF + checking will not be performed. + + If the value provided is a string, that string will be used as the "check + name". If the value provided is ``True``, ``csrf_token`` will be used as the + check name. + + If CSRF checking is performed, the checked value will be the value of + ``request.POST[check_name]``. This value will be compared against the + value of ``request.session.get_csrf_token()``, and the check will pass if + these two values are the same. If the check passes, the associated view will + be permitted to execute. If the check fails, the associated view will not be + permitted to execute. + + Note that using this feature requires a :term:`session factory` to have been + configured. + + .. versionadded:: 1.4a2 + +``physical_path`` + If specified, this value should be a string or a tuple representing the + :term:`physical path` of the context found via traversal for this predicate + to match as true. For example, ``physical_path='/'``, + ``physical_path='/a/b/c'``, or ``physical_path=('', 'a', 'b', 'c')``. This + is not a path prefix match or a regex, but a whole-path match. It's useful + when you want to always potentially show a view when some object is traversed + to, but you can't be sure about what kind of object it will be, so you can't + use the ``context`` predicate. The individual path elements between slash + characters or in tuple elements should be the Unicode representation of the + name of the resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + +``effective_principals`` + If specified, this value should be a :term:`principal` identifier or a + sequence of principal identifiers. If the + :meth:`pyramid.request.Request.effective_principals` method indicates that + every principal named in the argument list is present in the current request, + this predicate will return True; otherwise it will return False. For + example: ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + ``custom_predicates`` - If ``custom_predicates`` is specified, it must be a sequence of references - to custom predicate callables. Use custom predicates when no set of - predefined predicates do what you need. Custom predicates can be combined - with predefined predicates as necessary. Each custom predicate callable - should accept two arguments: ``context`` and ``request`` and should return - either ``True`` or ``False`` after doing arbitrary evaluation of the - context resource and/or the request. If all callables return ``True``, the + If ``custom_predicates`` is specified, it must be a sequence of references to + custom predicate callables. Use custom predicates when no set of predefined + predicates do what you need. Custom predicates can be combined with + predefined predicates as necessary. Each custom predicate callable should + accept two arguments, ``context`` and ``request``, and should return either + ``True`` or ``False`` after doing arbitrary evaluation of the context + resource and/or the request. If all callables return ``True``, the associated view callable will be considered viable for a given request. - If ``custom_predicates`` is not specified, no custom predicates are - used. + If ``custom_predicates`` is not specified, no custom predicates are used. + +``predicates`` + Pass a key/value pair here to use a third-party predicate registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than one + key/value pair can be used at the same time. See + :ref:`view_and_route_predicates` for more information about third-party + predicates. + + .. versionadded:: 1.4a1 + +Inverting Predicate Values +++++++++++++++++++++++++++ + +You can invert the meaning of any predicate value by wrapping it in a call to +:class:`pyramid.config.not_`. + +.. code-block:: python + :linenos: + + from pyramid.config import not_ + + config.add_view( + 'mypackage.views.my_view', + route_name='ok', + request_method=not_('POST') + ) + +The above example will ensure that the view is called if the request method is +*not* ``POST``, at least if no other view is more specific. + +This technique of wrapping a predicate value in ``not_`` can be used anywhere +predicate values are accepted: + +- :meth:`pyramid.config.Configurator.add_view` + +- :meth:`pyramid.view.view_config` + +.. versionadded:: 1.5 + .. index:: single: view_config decorator .. _mapping_views_using_a_decorator_section: -View Configuration Using the ``@view_config`` Decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For better locality of reference, you may use the -:class:`pyramid.view.view_config` decorator to associate your view functions -with URLs instead of using imperative configuration for the same purpose. +Adding View Configuration Using the ``@view_config`` Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. warning:: - Using this feature tends to slows down application startup slightly, as - more work is performed at application startup to scan for view - declarations. + Using this feature tends to slow down application startup slightly, as more + work is performed at application startup to scan for view configuration + declarations. For maximum startup performance, use the view configuration + method described in :ref:`mapping_views_using_imperative_config_section` + instead. -Usage of the ``view_config`` decorator is a form of :term:`declarative -configuration` in decorator form. :class:`~pyramid.view.view_config` can be -used to associate :term:`view configuration` information -- as done via the -equivalent imperative code -- with a function that acts as a :app:`Pyramid` -view callable. All arguments to the -:meth:`pyramid.config.Configurator.add_view` method (save for the ``view`` -argument) are available in decorator form and mean precisely the same thing. +The :class:`~pyramid.view.view_config` decorator can be used to associate +:term:`view configuration` information with a function, method, or class that +acts as a :app:`Pyramid` view callable. -An example of the :class:`~pyramid.view.view_config` decorator might reside in -a :app:`Pyramid` application module ``views.py``: +Here's an example of the :class:`~pyramid.view.view_config` decorator that +lives within a :app:`Pyramid` application module ``views.py``: -.. ignore-next-block .. code-block:: python :linenos: @@ -388,20 +580,18 @@ a :app:`Pyramid` application module ``views.py``: from pyramid.view import view_config from pyramid.response import Response - @view_config(name='my_view', request_method='POST', context=MyResource, - permission='read') + @view_config(route_name='ok', request_method='POST', permission='read') def my_view(request): return Response('OK') Using this decorator as above replaces the need to add this imperative configuration stanza: -.. ignore-next-block .. code-block:: python :linenos: - config.add_view('mypackage.views.my_view', name='my_view', request_method='POST', - context=MyResource, permission='read') + config.add_view('mypackage.views.my_view', route_name='ok', + request_method='POST', permission='read') All arguments to ``view_config`` may be omitted. For example: @@ -424,9 +614,8 @@ request method, request type, request param, route name, or containment. The mere existence of a ``@view_config`` decorator doesn't suffice to perform view configuration. All that the decorator does is "annotate" the function with your configuration declarations, it doesn't process them. To make -:app:`Pyramid` process your :class:`pyramid.view.view_config` declarations, -you *must* use the ``scan`` method of a -:class:`pyramid.config.Configurator`: +:app:`Pyramid` process your :class:`pyramid.view.view_config` declarations, you +*must* use the ``scan`` method of a :class:`pyramid.config.Configurator`: .. code-block:: python :linenos: @@ -435,15 +624,28 @@ you *must* use the ``scan`` method of a # pyramid.config.Configurator class config.scan() -Please see :ref:`decorations_and_code_scanning` for detailed information -about what happens when code is scanned for configuration declarations -resulting from use of decorators like :class:`~pyramid.view.view_config`. +Please see :ref:`decorations_and_code_scanning` for detailed information about +what happens when code is scanned for configuration declarations resulting from +use of decorators like :class:`~pyramid.view.view_config`. See :ref:`configuration_module` for additional API arguments to the :meth:`~pyramid.config.Configurator.scan` method. For example, the method allows you to supply a ``package`` argument to better control exactly *which* code will be scanned. +All arguments to the :class:`~pyramid.view.view_config` decorator mean +precisely the same thing as they would if they were passed as arguments to the +:meth:`pyramid.config.Configurator.add_view` method save for the ``view`` +argument. Usage of the :class:`~pyramid.view.view_config` decorator is a form +of :term:`declarative configuration`, while +:meth:`pyramid.config.Configurator.add_view` is a form of :term:`imperative +configuration`. However, they both do the same thing. + +.. index:: + single: view_config placement + +.. _view_config_placement: + ``@view_config`` Placement ++++++++++++++++++++++++++ @@ -458,32 +660,13 @@ If your view callable is a function, it may be used as a function decorator: from pyramid.view import view_config from pyramid.response import Response - @view_config(name='edit') + @view_config(route_name='edit') def edit(request): return Response('edited!') If your view callable is a class, the decorator can also be used as a class -decorator in Python 2.6 and better (Python 2.5 and below do not support class -decorators). All the arguments to the decorator are the same when applied -against a class as when they are applied against a function. For example: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - @view_config() - class MyView(object): - def __init__(self, request): - self.request = request - - def __call__(self): - return Response('hello') - -You can use the :class:`~pyramid.view.view_config` decorator as a simple -callable to manually decorate classes in Python 2.5 and below without the -decorator syntactic sugar, if you wish: +decorator. All the arguments to the decorator are the same when applied against +a class as when they are applied against a function. For example: .. code-block:: python :linenos: @@ -491,6 +674,7 @@ decorator syntactic sugar, if you wish: from pyramid.response import Response from pyramid.view import view_config + @view_config(route_name='hello') class MyView(object): def __init__(self, request): self.request = request @@ -498,8 +682,6 @@ decorator syntactic sugar, if you wish: def __call__(self): return Response('hello') - my_view = view_config()(MyView) - More than one :class:`~pyramid.view.view_config` decorator can be stacked on top of any number of others. Each decorator creates a separate view registration. For example: @@ -510,8 +692,8 @@ registration. For example: from pyramid.view import view_config from pyramid.response import Response - @view_config(name='edit') - @view_config(name='change') + @view_config(route_name='edit') + @view_config(route_name='change') def edit(request): return Response('edited!') @@ -529,21 +711,21 @@ The decorator can also be used against a method of a class: def __init__(self, request): self.request = request - @view_config(name='hello') + @view_config(route_name='hello') def amethod(self): return Response('hello') When the decorator is used against a method of a class, a view is registered for the *class*, so the class constructor must accept an argument list in one -of two forms: either it must accept a single argument ``request`` or it must -accept two arguments, ``context, request``. +of two forms: either a single argument, ``request``, or two arguments, +``context, request``. The method which is decorated must return a :term:`response`. Using the decorator against a particular method of a class is equivalent to -using the ``attr`` parameter in a decorator attached to the class itself. -For example, the above registration implied by the decorator being used -against the ``amethod`` method could be spelled equivalently as the below: +using the ``attr`` parameter in a decorator attached to the class itself. For +example, the above registration implied by the decorator being used against the +``amethod`` method could be written equivalently as follows: .. code-block:: python :linenos: @@ -551,7 +733,7 @@ against the ``amethod`` method could be spelled equivalently as the below: from pyramid.response import Response from pyramid.view import view_config - @view_config(attr='amethod', name='hello') + @view_config(attr='amethod', route_name='hello') class MyView(object): def __init__(self, request): self.request = request @@ -559,18 +741,20 @@ against the ``amethod`` method could be spelled equivalently as the below: def amethod(self): return Response('hello') + .. index:: single: add_view .. _mapping_views_using_imperative_config_section: -View Registration Using :meth:`~pyramid.config.Configurator.add_view` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adding View Configuration Using :meth:`~pyramid.config.Configurator.add_view` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :meth:`pyramid.config.Configurator.add_view` method within -:ref:`configuration_module` is used to configure a view imperatively. The -arguments to this method are very similar to the arguments that you provide -to the ``@view_config`` decorator. For example: +:ref:`configuration_module` is used to configure a view "imperatively" (without +a :class:`~pyramid.view.view_config` decorator). The arguments to this method +are very similar to the arguments that you provide to the +:class:`~pyramid.view.view_config` decorator. For example: .. code-block:: python :linenos: @@ -582,108 +766,196 @@ to the ``@view_config`` decorator. For example: # config is assumed to be an instance of the # pyramid.config.Configurator class - config.add_view(hello_world, name='hello.html') + config.add_view(hello_world, route_name='hello') + +The first argument, a :term:`view callable`, is the only required argument. It +must either be a Python object which is the view itself or a :term:`dotted +Python name` to such an object. In the above example, the ``view callable`` is +the ``hello_world`` function. -The first argument, ``view``, is required. It must either be a Python object -which is the view itself or a :term:`dotted Python name` to such an object. -All other arguments are optional. See -:meth:`pyramid.config.Configurator.add_view` for more information. +When you use only :meth:`~pyramid.config.Configurator.add_view` to add view +configurations, you don't need to issue a :term:`scan` in order for the view +configuration to take effect. .. index:: - single: resource interfaces + single: view_defaults class decorator -.. _using_resource_interfaces: +.. _view_defaults: -Using Resource Interfaces In View Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``@view_defaults`` Class Decorator +---------------------------------- -Instead of registering your views with a ``context`` that names a Python -resource *class*, you can optionally register a view callable with a -``context`` which is an :term:`interface`. An interface can be attached -arbitrarily to any resource object. View lookup treats context interfaces -specially, and therefore the identity of a resource can be divorced from that -of the class which implements it. As a result, associating a view with an -interface can provide more flexibility for sharing a single view between two -or more different implementations of a resource type. For example, if two -resource objects of different Python class types share the same interface, -you can use the same view configuration to specify both of them as a -``context``. +.. versionadded:: 1.3 -In order to make use of interfaces in your application during view dispatch, -you must create an interface and mark up your resource classes or instances -with interface declarations that refer to this interface. +If you use a class as a view, you can use the +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. -To attach an interface to a resource *class*, you define the interface and -use the :func:`zope.interface.implements` function to associate the interface -with the class. +For instance, if you've got a class that has methods that represent "REST +actions", all of which are mapped to the same route but different request +methods, instead of this: .. code-block:: python :linenos: - from zope.interface import Interface - from zope.interface import implements + from pyramid.view import view_config + from pyramid.response import Response - class IHello(Interface): - """ A marker interface """ + class RESTView(object): + def __init__(self, request): + self.request = request - class Hello(object): - implements(IHello) + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') -To attach an interface to a resource *instance*, you define the interface and -use the :func:`zope.interface.alsoProvides` function to associate the -interface with the instance. This function mutates the instance in such a -way that the interface is attached to it. + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: .. code-block:: python :linenos: - from zope.interface import Interface - from zope.interface import alsoProvides + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response - class IHello(Interface): - """ A marker interface """ + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request - class Hello(object): - pass + @view_config(request_method='GET') + def get(self): + return Response('get') - def make_hello(): - hello = Hello() - alsoProvides(hello, IHello) - return hello + @view_config(request_method='POST') + def post(self): + return Response('post') -Regardless of how you associate an interface, with a resource instance, or a -resource class, the resulting code to associate that interface with a view -callable is the same. Assuming the above code that defines an ``IHello`` -interface lives in the root of your application, and its module is named -"resources.py", the interface declaration below will associate the -``mypackage.views.hello_world`` view with resources that implement, or -provide, this interface. + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +In the above example, we were able to take the ``route_name='rest'`` argument +out of the call to each individual ``@view_config`` statement because we used a +``@view_defaults`` class decorator to provide the argument as a default to each +view method it possessed. + +Arguments passed to ``@view_config`` will override any default passed to +``@view_defaults``. + +The ``view_defaults`` class decorator can also provide defaults to the +:meth:`pyramid.config.Configurator.add_view` directive when a decorated class +is passed to that directive as its ``view`` argument. For example, instead of +this: .. code-block:: python :linenos: - # config is an instance of pyramid.config.Configurator + from pyramid.response import Response + from pyramid.config import Configurator + + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + def main(global_config, **settings): + config = Configurator() + config.add_route('rest', '/rest') + config.add_view( + RESTView, route_name='rest', attr='get', request_method='GET') + config.add_view( + RESTView, route_name='rest', attr='post', request_method='POST') + config.add_view( + RESTView, route_name='rest', attr='delete', request_method='DELETE') + return config.make_wsgi_app() + +To reduce the amount of repetition in the ``config.add_view`` statements, we +can move the ``route_name='rest'`` argument to a ``@view_defaults`` class +decorator on the ``RESTView`` class: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.response import Response + from pyramid.config import Configurator + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + def main(global_config, **settings): + config = Configurator() + config.add_route('rest', '/rest') + config.add_view(RESTView, attr='get', request_method='GET') + config.add_view(RESTView, attr='post', request_method='POST') + config.add_view(RESTView, attr='delete', request_method='DELETE') + return config.make_wsgi_app() + +:class:`pyramid.view.view_defaults` accepts the same set of arguments that +:class:`pyramid.view.view_config` does, and they have the same meaning. Each +argument passed to ``view_defaults`` provides a default for the view +configurations of methods of the class it's decorating. + +Normal Python inheritance rules apply to defaults added via ``view_defaults``. +For example: + +.. code-block:: python + :linenos: - config.add_view('mypackage.views.hello_world', name='hello.html', - context='mypackage.resources.IHello') + @view_defaults(route_name='rest') + class Foo(object): + pass + + class Bar(Foo): + pass + +The ``Bar`` class above will inherit its view defaults from the arguments +passed to the ``view_defaults`` decorator of the ``Foo`` class. To prevent +this from happening, use a ``view_defaults`` decorator without any arguments on +the subclass: -Any time a resource that is determined to be the :term:`context` provides -this interface, and a view named ``hello.html`` is looked up against it as -per the URL, the ``mypackage.views.hello_world`` view callable will be -invoked. +.. code-block:: python + :linenos: -Note, in cases where a view is registered against a resource class, and a -view is also registered against an interface that the resource class -implements, an ambiguity arises. Views registered for the resource class take -precedence over any views registered for any interface the resource class -implements. Thus, if one view configuration names a ``context`` of both the -class type of a resource, and another view configuration names a ``context`` -of interface implemented by the resource's class, and both view -configurations are otherwise identical, the view registered for the context's -class will "win". + @view_defaults(route_name='rest') + class Foo(object): + pass -For more information about defining resources with interfaces for use within -view configuration, see :ref:`resources_which_implement_interfaces`. + @view_defaults() + class Bar(Foo): + pass + +The ``view_defaults`` decorator only works as a class decorator; using it +against a function or a method will produce nonsensical results. .. index:: single: view security @@ -695,25 +967,26 @@ Configuring View Security ~~~~~~~~~~~~~~~~~~~~~~~~~ If an :term:`authorization policy` is active, any :term:`permission` attached -to a :term:`view configuration` found during view lookup will be verified. -This will ensure that the currently authenticated user possesses that -permission against the :term:`context` resource before the view function is -actually called. Here's an example of specifying a permission in a view -configuration using :meth:`~pyramid.config.Configurator.add_view`: +to a :term:`view configuration` found during view lookup will be verified. This +will ensure that the currently authenticated user possesses that permission +against the :term:`context` resource before the view function is actually +called. Here's an example of specifying a permission in a view configuration +using :meth:`~pyramid.config.Configurator.add_view`: .. code-block:: python :linenos: # config is an instance of pyramid.config.Configurator - config.add_view('myproject.views.add_entry', name='add.html', - context='myproject.resources.IBlog', permission='add') + config.add_route('add', '/add.html', factory='mypackage.Blog') + config.add_view('myproject.views.add_entry', route_name='add', + permission='add') When an :term:`authorization policy` is enabled, this view will be protected with the ``add`` permission. The view will *not be called* if the user does not possess the ``add`` permission relative to the current :term:`context`. -Instead the :term:`forbidden view` result will be returned to the client as -per :ref:`protecting_views`. +Instead the :term:`forbidden view` result will be returned to the client as per +:ref:`protecting_views`. .. index:: single: debugging not found errors @@ -721,111 +994,80 @@ per :ref:`protecting_views`. .. _debug_notfound_section: -:exc:`NotFound` Errors -~~~~~~~~~~~~~~~~~~~~~~ +:exc:`~pyramid.exceptions.NotFound` Errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -It's useful to be able to debug :exc:`NotFound` error responses when they -occur unexpectedly due to an application registry misconfiguration. To debug -these errors, use the ``PYRAMID_DEBUG_NOTFOUND`` environment variable or the -``debug_notfound`` configuration file setting. Details of why a view was not -found will be printed to ``stderr``, and the browser representation of the -error will include the same information. See :ref:`environment_chapter` for -more information about how, and where to set these values. +It's useful to be able to debug :exc:`~pyramid.exceptions.NotFound` error +responses when they occur unexpectedly due to an application registry +misconfiguration. To debug these errors, use the ``PYRAMID_DEBUG_NOTFOUND`` +environment variable or the ``pyramid.debug_notfound`` configuration file +setting. Details of why a view was not found will be printed to ``stderr``, +and the browser representation of the error will include the same information. +See :ref:`environment_chapter` for more information about how, and where to set +these values. .. index:: - pair: matching views; printing - single: paster pviews + single: HTTP caching -.. _displaying_matching_views: +.. _influencing_http_caching: -Displaying Matching Views for a Given URL ------------------------------------------ +Influencing HTTP Caching +------------------------ -For a big application with several views, it can be hard to keep the view -configuration details in your head, even if you defined all the views -yourself. You can use the ``paster pviews`` command in a terminal window to -print a summary of matching routes and views for a given URL in your -application. The ``paster pviews`` command accepts three arguments. The -first argument to ``pviews`` is the path to your application's ``.ini`` file. -The second is the ``app`` section name inside the ``.ini`` file which points -to your application. The third is the URL to test for matching views. +.. versionadded:: 1.1 -Here is an example for a simple view configuration using :term:`traversal`: +When a non-``None`` ``http_cache`` argument is passed to a view configuration, +Pyramid will set ``Expires`` and ``Cache-Control`` response headers in the +resulting response, causing browsers to cache the response data for some time. +See ``http_cache`` in :ref:`nonpredicate_view_args` for the allowable values +and what they mean. -.. code-block:: text - :linenos: +Sometimes it's undesirable to have these headers set as the result of returning +a response from a view, even though you'd like to decorate the view with a view +configuration decorator that has ``http_cache``. Perhaps there's an +alternative branch in your view code that returns a response that should never +be cacheable, while the "normal" branch returns something that should always be +cacheable. If this is the case, set the ``prevent_auto`` attribute of the +``response.cache_control`` object to a non-``False`` value. For example, the +below view callable is configured with a ``@view_config`` decorator that +indicates any response from the view should be cached for 3600 seconds. +However, the view itself prevents caching from taking place unless there's a +``should_cache`` GET or POST variable: - $ ../bin/paster pviews development.ini tutorial /FrontPage +.. code-block:: python - URL = /FrontPage + from pyramid.view import view_config - context: <tutorial.models.Page object at 0xa12536c> - view name: + @view_config(http_cache=3600) + def view(request): + response = Response() + if 'should_cache' not in request.params: + response.cache_control.prevent_auto = True + return response - View: - ----- - tutorial.views.view_page - required permission = view +Note that the ``http_cache`` machinery will overwrite or add to caching headers +you set within the view itself, unless you use ``prevent_auto``. -The output always has the requested URL at the top and below that all the -views that matched with their view configuration details. In this example -only one view matches, so there is just a single *View* section. For each -matching view, the full code path to the associated view callable is shown, -along with any permissions and predicates that are part of that view -configuration. +You can also turn off the effect of ``http_cache`` entirely for the duration of +a Pyramid application lifetime. To do so, set the +``PYRAMID_PREVENT_HTTP_CACHE`` environment variable or the +``pyramid.prevent_http_cache`` configuration value setting to a true value. For +more information, see :ref:`preventing_http_caching`. -A more complex configuration might generate something like this: +Note that setting ``pyramid.prevent_http_cache`` will have no effect on caching +headers that your application code itself sets. It will only prevent caching +headers that would have been set by the Pyramid HTTP caching machinery invoked +as the result of the ``http_cache`` argument to view configuration. -.. code-block:: text - :linenos: +.. index:: + pair: view configuration; debugging + +.. _debugging_view_configuration: - $ ../bin/paster pviews development.ini shootout /about - - URL = /about - - context: <shootout.models.RootFactory object at 0xa56668c> - view name: about - - Route: - ------ - route name: about - route pattern: /about - route path: /about - subpath: - route predicates (request method = GET) - - View: - ----- - shootout.views.about_view - required permission = view - view predicates (request_param testing, header X/header) - - Route: - ------ - route name: about_post - route pattern: /about - route path: /about - subpath: - route predicates (request method = POST) - - View: - ----- - shootout.views.about_view_post - required permission = view - view predicates (request_param test) - - View: - ----- - shootout.views.about_view_post2 - required permission = view - view predicates (request_param test2) - -In this case, we are dealing with a :term:`URL dispatch` application. This -specific URL has two matching routes. The matching route information is -displayed first, followed by any views that are associated with that route. -As you can see from the second matching route output, a route can be -associated with more than one view. - -For a URL that doesn't match any views, ``paster pviews`` will simply print -out a *Not found* message. +Debugging View Configuration +---------------------------- +See :ref:`displaying_matching_views` for information about how to display +each of the view callables that might match for a given URL. This can be an +effective way to figure out why a particular view callable is being called +instead of the one you'd like to be called. diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 5c9bd91af..770d27919 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -3,62 +3,45 @@ Views ===== -One of the primary jobs of :app:`Pyramid` is to find and invoke a -:term:`view callable` when a :term:`request` reaches your application. View -callables are bits of code which do something interesting in response to a -request made to your application. +One of the primary jobs of :app:`Pyramid` is to find and invoke a :term:`view +callable` when a :term:`request` reaches your application. View callables are +bits of code which do something interesting in response to a request made to +your application. They are the "meat" of any interesting web application. -.. note:: +.. note:: A :app:`Pyramid` :term:`view callable` is often referred to in - conversational shorthand as a :term:`view`. In this documentation, - however, we need to use less ambiguous terminology because there - are significant differences between view *configuration*, the code - that implements a view *callable*, and the process of view - *lookup*. - -The :ref:`urldispatch_chapter`, and :ref:`traversal_chapter` chapters -describes how, using information from the :term:`request`, a -:term:`context` resource is computed. But the context resource itself -isn't very useful without an associated :term:`view callable`. A view -callable returns a response to a user, often using the context resource -to do so. - -The job of actually locating and invoking the "best" :term:`view callable` is -the job of the :term:`view lookup` subsystem. The view lookup subsystem -compares the resource supplied by :term:`resource location` and information -in the :term:`request` against :term:`view configuration` statements made by -the developer to choose the most appropriate view callable for a specific -set of circumstances. - -This chapter describes how view callables work. In the -:ref:`view_config_chapter` chapter, there are details about performing -view configuration, and a detailed explanation of view lookup. + conversational shorthand as a :term:`view`. In this documentation, however, + we need to use less ambiguous terminology because there are significant + differences between view *configuration*, the code that implements a view + *callable*, and the process of view *lookup*. + +This chapter describes how view callables should be defined. We'll have to wait +until a following chapter (entitled :ref:`view_config_chapter`) to find out how +we actually tell :app:`Pyramid` to wire up view callables to particular URL +patterns and other request circumstances. + +.. index:: + single: view callables View Callables -------------- -View callables are, at the risk of sounding obvious, callable Python -objects. Specifically, view callables can be functions, classes, or -instances that implement an ``__call__`` method (making the -instance callable). - -View callables must, at a minimum, accept a single argument named -``request``. This argument represents a :app:`Pyramid` :term:`Request` -object. A request object encapsulates a WSGI environment provided to -:app:`Pyramid` by the upstream :term:`WSGI` server. As you might expect, -the request object contains everything your application needs to know -about the specific HTTP request being made. - -A view callable's ultimate responsibility is to create a :mod:`Pyramid` -:term:`Response` object. This can be done by creating the response -object in the view callable code and returning it directly, as we will -be doing in this chapter. However, if a view callable does not return a -response itself, it can be configured to use a :term:`renderer` that -converts its return value into a :term:`Response` object. Using -renderers is the common way that templates are used with view callables -to generate markup. See the :ref:`renderers_chapter` chapter for -details. +View callables are, at the risk of sounding obvious, callable Python objects. +Specifically, view callables can be functions, classes, or instances that +implement a ``__call__`` method (making the instance callable). + +View callables must, at a minimum, accept a single argument named ``request``. +This argument represents a :app:`Pyramid` :term:`Request` object. A request +object represents a :term:`WSGI` environment provided to :app:`Pyramid` by the +upstream WSGI server. As you might expect, the request object contains +everything your application needs to know about the specific HTTP request being +made. + +A view callable's ultimate responsibility is to create a :app:`Pyramid` +:term:`Response` object. This can be done by creating a :term:`Response` object +in the view callable code and returning it directly or by raising special kinds +of exceptions from within the body of a view callable. .. index:: single: view calling convention @@ -92,17 +75,17 @@ Defining a View Callable as a Class ----------------------------------- A view callable may also be represented by a Python class instead of a -function. When a view callable is a class, the calling semantics are -slightly different than when it is a function or another non-class callable. -When a view callable is a class, the class' ``__init__`` method is called with a +function. When a view callable is a class, the calling semantics are slightly +different than when it is a function or another non-class callable. When a view +callable is a class, the class's ``__init__`` method is called with a ``request`` parameter. As a result, an instance of the class is created. Subsequently, that instance's ``__call__`` method is invoked with no -parameters. Views defined as classes must have the following traits: +parameters. Views defined as classes must have the following traits. -- an ``__init__`` method that accepts a ``request`` argument. +- an ``__init__`` method that accepts a ``request`` argument -- a ``__call__`` (or other) method that accepts no parameters and which - returns a response. +- a ``__call__`` (or other) method that accepts no parameters and which returns + a response For example: @@ -122,91 +105,12 @@ The request object passed to ``__init__`` is the same type of request object described in :ref:`function_as_view`. If you'd like to use a different attribute than ``__call__`` to represent the -method expected to return a response, you can use an ``attr`` value as part -of the configuration for the view. See :ref:`view_configuration_parameters`. -The same view callable class can be used in different view configuration -statements with different ``attr`` values, each pointing at a different -method of the class if you'd like the class to represent a collection of -related view callables. - -.. note:: A package named :term:`pyramid_handlers` (available from PyPI) - provides an analogue of :term:`Pylons` -style "controllers", which are a - special kind of view class which provides more automation when your - application uses :term:`URL dispatch` solely. - -.. index:: - single: view calling convention - -.. _request_and_context_view_definitions: - -Alternate View Callable Argument/Calling Conventions ----------------------------------------------------- - -Usually, view callables are defined to accept only a single argument: -``request``. However, view callables may alternately be defined as classes, -functions, or any callable that accept *two* positional arguments: a -:term:`context` resource as the first argument and a :term:`request` as the -second argument. - -The :term:`context` and :term:`request` arguments passed to a view function -defined in this style can be defined as follows: - -context - - The :term:`resource` object found via tree :term:`traversal` or :term:`URL - dispatch`. - -request - A :app:`Pyramid` Request object representing the current WSGI request. - -The following types work as view callables in this style: - -#. Functions that accept two arguments: ``context``, and ``request``, - e.g.: - - .. code-block:: python - :linenos: - - from pyramid.response import Response - - def view(context, request): - return Response('OK') - -#. Classes that have an ``__init__`` method that accepts ``context, - request`` and a ``__call__`` method which accepts no arguments, e.g.: - - .. code-block:: python - :linenos: - - from pyramid.response import Response - - class view(object): - def __init__(self, context, request): - self.context = context - self.request = request - - def __call__(self): - return Response('OK') - -#. Arbitrary callables that have a ``__call__`` method that accepts - ``context, request``, e.g.: - - .. code-block:: python - :linenos: - - from pyramid.response import Response - - class View(object): - def __call__(self, context, request): - return Response('OK') - view = View() # this is the view callable - -This style of calling convention is most useful for :term:`traversal` based -applications, where the context object is frequently used within the view -callable code itself. - -No matter which view calling convention is used, the view code always has -access to the context via ``request.context``. +method expected to return a response, you can use an ``attr`` value as part of +the configuration for the view. See :ref:`view_configuration_parameters`. The +same view callable class can be used in different view configuration statements +with different ``attr`` values, each pointing at a different method of the +class if you'd like the class to represent a collection of related view +callables. .. index:: single: view response @@ -230,117 +134,138 @@ implements the :term:`Response` interface is to return a def view(request): return Response('OK') -You don't need to always use :class:`~pyramid.response.Response` to represent -a response. :app:`Pyramid` provides a range of different "exception" classes -which can act as response objects too. For example, an instance of the class +:app:`Pyramid` provides a range of different "exception" classes which inherit +from :class:`pyramid.response.Response`. For example, an instance of the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response object -(see :ref:`http_redirect`). A view can actually return any object that has -the following attributes. +because it inherits from :class:`~pyramid.response.Response`. For examples, +see :ref:`http_exceptions` and :ref:`http_redirect`. -status - The HTTP status code (including the name) for the response as a string. - E.g. ``200 OK`` or ``401 Unauthorized``. +.. note:: -headerlist - A sequence of tuples representing the list of headers that should be - set in the response. E.g. ``[('Content-Type', 'text/html'), - ('Content-Length', '412')]`` + You can also return objects from view callables that aren't instances of + :class:`pyramid.response.Response` in various circumstances. This can be + helpful when writing tests and when attempting to share code between view + callables. See :ref:`renderers_chapter` for the common way to allow for + this. A much less common way to allow for view callables to return + non-Response objects is documented in :ref:`using_iresponse`. -app_iter - An iterable representing the body of the response. This can be a - list, e.g. ``['<html><head></head><body>Hello - world!</body></html>']`` or it can be a file-like object, or any - other sort of iterable. +.. index:: + single: view exceptions + +.. _special_exceptions_in_callables: -These attributes form the structure of the "Pyramid Response interface". +Using Special Exceptions in View Callables +------------------------------------------ + +Usually when a Python exception is raised within a view callable, +:app:`Pyramid` allows the exception to propagate all the way out to the +:term:`WSGI` server which invoked the application. It is usually caught and +logged there. + +However, for convenience, a special set of exceptions exists. When one of +these exceptions is raised within a view callable, it will always cause +:app:`Pyramid` to generate a response. These are known as :term:`HTTP +exception` objects. .. index:: - single: view http redirect - single: http redirect (from a view) + single: HTTP exceptions -.. _http_redirect: +.. _http_exceptions: -Using a View Callable to Do an HTTP Redirect --------------------------------------------- +HTTP Exceptions +~~~~~~~~~~~~~~~ + +All :mod:`pyramid.httpexceptions` classes which are documented as inheriting +from the :class:`pyramid.httpexceptions.HTTPException` are :term:`http +exception` objects. Instances of an HTTP exception object may either be +*returned* or *raised* from within view code. In either case (return or raise) +the instance will be used as the view's response. -You can issue an HTTP redirect from within a view by returning a particular -kind of response. +For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception can +be raised. This will cause a response to be generated with a ``401 +Unauthorized`` status: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.httpexceptions import HTTPUnauthorized - def myview(request): - return HTTPFound(location='http://example.com') + def aview(request): + raise HTTPUnauthorized() -All exception types from the :mod:`pyramid.httpexceptions` module implement -the :term:`Response` interface; any can be returned as the response from a -view. See :mod:`pyramid.httpexceptions` for the documentation for the -``HTTPFound`` exception; it also includes other response types that imply -other HTTP response codes, such as ``HTTPUnauthorized`` for ``401 -Unauthorized``. +An HTTP exception, instead of being raised, can alternately be *returned* (HTTP +exceptions are also valid response objects): -.. note:: +.. code-block:: python + :linenos: - Although exception types from the :mod:`pyramid.httpexceptions` module are - in fact bona fide Python :class:`Exception` types, the :app:`Pyramid` view - machinery expects them to be *returned* by a view callable rather than - *raised*. + from pyramid.httpexceptions import HTTPUnauthorized - It is possible, however, in Python 2.5 and above, to configure an - *exception view* to catch these exceptions, and return an appropriate - :class:`~pyramid.response.Response`. The simplest such view could just - catch and return the original exception. See :ref:`exception_views` for - more details. + def aview(request): + return HTTPUnauthorized() -.. index:: - single: view exceptions +A shortcut for creating an HTTP exception is the +:func:`pyramid.httpexceptions.exception_response` function. This function +accepts an HTTP status code and returns the corresponding HTTP exception. For +example, instead of importing and constructing a +:class:`~pyramid.httpexceptions.HTTPUnauthorized` response object, you can use +the :func:`~pyramid.httpexceptions.exception_response` function to construct +and return the same object. -.. _special_exceptions_in_callables: +.. code-block:: python + :linenos: -Using Special Exceptions In View Callables ------------------------------------------- + from pyramid.httpexceptions import exception_response -Usually when a Python exception is raised within a view callable, -:app:`Pyramid` allows the exception to propagate all the way out to the -:term:`WSGI` server which invoked the application. + def aview(request): + raise exception_response(401) + +This is the case because ``401`` is the HTTP status code for "HTTP +Unauthorized". Therefore, ``raise exception_response(401)`` is functionally +equivalent to ``raise HTTPUnauthorized()``. Documentation which maps each HTTP +response code to its purpose and its associated HTTP exception object is +provided within :mod:`pyramid.httpexceptions`. -However, for convenience, two special exceptions exist which are always -handled by :app:`Pyramid` itself. These are -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden`. -Both are exception classes which accept a single positional constructor -argument: a ``message``. +.. versionadded:: 1.1 + The :func:`~pyramid.httpexceptions.exception_response` function. -If :exc:`~pyramid.exceptions.NotFound` is raised within view code, the result -of the :term:`Not Found View` will be returned to the user agent which -performed the request. +How Pyramid Uses HTTP Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If :exc:`~pyramid.exceptions.Forbidden` is raised within view code, the result -of the :term:`Forbidden View` will be returned to the user agent which -performed the request. +HTTP exceptions are meant to be used directly by application developers. +However, Pyramid itself will raise two HTTP exceptions at various points during +normal operations. -In all cases, the message provided to the exception constructor is made -available to the view which :app:`Pyramid` invokes as -``request.exception.args[0]``. +* :exc:`~pyramid.httpexceptions.HTTPNotFound` gets raised when a view to + service a request is not found. +* :exc:`~pyramid.httpexceptions.HTTPForbidden` gets raised when authorization + was forbidden by a security policy. + +If :exc:`~pyramid.httpexceptions.HTTPNotFound` is raised by Pyramid itself or +within view code, the result of the :term:`Not Found View` will be returned to +the user agent which performed the request. + +If :exc:`~pyramid.httpexceptions.HTTPForbidden` is raised by Pyramid itself +within view code, the result of the :term:`Forbidden View` will be returned to +the user agent which performed the request. .. index:: single: exception views .. _exception_views: -Exception Views ---------------- +Custom Exception Views +---------------------- -The machinery which allows the special :exc:`~pyramid.exceptions.NotFound` and -:exc:`~pyramid.exceptions.Forbidden` exceptions to be caught by specialized -views as described in :ref:`special_exceptions_in_callables` can also be used -by application developers to convert arbitrary exceptions to responses. +The machinery which allows HTTP exceptions to be raised and caught by +specialized views as described in :ref:`special_exceptions_in_callables` can +also be used by application developers to convert arbitrary exceptions to +responses. To register a view that should be called whenever a particular exception is -raised from with :app:`Pyramid` view code, use the exception class or one of -its superclasses as the ``context`` of a view configuration which points at a -view callable you'd like to generate a response. +raised from within :app:`Pyramid` view code, use the exception class (or one of +its superclasses) as the :term:`context` of a view configuration which points +at a view callable for which you'd like to generate a response. For example, given the following exception class in a module named ``helloworld.exceptions``: @@ -359,6 +284,7 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: .. code-block:: python :linenos: + from pyramid.view import view_config from helloworld.exceptions import ValidationFailure @view_config(context=ValidationFailure) @@ -370,44 +296,91 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: Assuming that a :term:`scan` was run to pick up this view registration, this view callable will be invoked whenever a ``helloworld.exceptions.ValidationFailure`` is raised by your application's -view code. The same exception raised by a custom root factory or a custom -traverser is also caught and hooked. +view code. The same exception raised by a custom root factory, a custom +traverser, or a custom view or route predicate is also caught and hooked. -Other normal view predicates can also be used in combination with an -exception view registration: +Other normal view predicates can also be used in combination with an exception +view registration: .. code-block:: python :linenos: from pyramid.view import view_config - from pyramid.exceptions import NotFound - from pyramid.httpexceptions import HTTPNotFound + from helloworld.exceptions import ValidationFailure - @view_config(context=NotFound, route_name='home') - def notfound_view(request): - return HTTPNotFound() + @view_config(context=ValidationFailure, route_name='home') + def failed_validation(exc, request): + response = Response('Failed validation: %s' % exc.msg) + response.status_int = 500 + return response -The above exception view names the ``route_name`` of ``home``, meaning that -it will only be called when the route matched has a name of ``home``. You -can therefore have more than one exception view for any given exception in -the system: the "most specific" one will be called when the set of request +The above exception view names the ``route_name`` of ``home``, meaning that it +will only be called when the route matched has a name of ``home``. You can +therefore have more than one exception view for any given exception in the +system: the "most specific" one will be called when the set of request circumstances match the view registration. -The only view predicate that cannot be used successfully when creating -an exception view configuration is ``name``. The name used to look up -an exception view is always the empty string. Views registered as -exception views which have a name will be ignored. +The only view predicate that cannot be used successfully when creating an +exception view configuration is ``name``. The name used to look up an +exception view is always the empty string. Views registered as exception views +which have a name will be ignored. .. note:: - Normal (i.e., non-exception) views registered against a context resource - type which inherits from :exc:`Exception` will work normally. When an - exception view configuration is processed, *two* views are registered. One - as a "normal" view, the other as an "exception" view. This means that you - can use an exception as ``context`` for a normal view. + Normal (i.e., non-exception) views registered against a context resource type + which inherits from :exc:`Exception` will work normally. When an exception + view configuration is processed, *two* views are registered. One as a + "normal" view, the other as an "exception" view. This means that you can use + an exception as ``context`` for a normal view. Exception views can be configured with any view registration mechanism: -``@view_config`` decorator, ZCML, or imperative ``add_view`` styles. +``@view_config`` decorator or imperative ``add_view`` styles. + +.. note:: + + Pyramid's :term:`exception view` handling logic is implemented as a tween + factory function: :func:`pyramid.tweens.excview_tween_factory`. If Pyramid + exception view handling is desired, and tween factories are specified via + the ``pyramid.tweens`` configuration setting, the + :func:`pyramid.tweens.excview_tween_factory` function must be added to the + ``pyramid.tweens`` configuration setting list explicitly. If it is not + present, Pyramid will not perform exception view handling. + +.. index:: + single: view http redirect + single: http redirect (from a view) + +.. _http_redirect: + +Using a View Callable to do an HTTP Redirect +-------------------------------------------- + +You can issue an HTTP redirect by using the +:class:`pyramid.httpexceptions.HTTPFound` class. Raising or returning an +instance of this class will cause the client to receive a "302 Found" response. + +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` instance. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + return HTTPFound(location='http://example.com') + +Alternately, you can *raise* an HTTPFound exception instead of returning one. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + raise HTTPFound(location='http://example.com') + +When the instance is raised, it is caught by the default :term:`exception +response` handler and turned into a response. .. index:: single: unicode, views, and forms @@ -422,34 +395,33 @@ various other clients. In :app:`Pyramid`, form submission handling logic is always part of a :term:`view`. For a general overview of how to handle form submission data using the :term:`WebOb` API, see :ref:`webob_chapter` and `"Query and POST variables" within the WebOb documentation -<http://pythonpaste.org/webob/reference.html#query-post-variables>`_. +<http://docs.webob.org/en/latest/reference.html#query-post-variables>`_. :app:`Pyramid` defers to WebOb for its request and response implementations, -and handling form submission data is a property of the request -implementation. Understanding WebOb's request API is the key to -understanding how to process form submission data. - -There are some defaults that you need to be aware of when trying to handle -form submission data in a :app:`Pyramid` view. Having high-order (i.e., -non-ASCII) characters in data contained within form submissions is -exceedingly common, and the UTF-8 encoding is the most common encoding used -on the web for character data. Since Unicode values are much saner than -working with and storing bytestrings, :app:`Pyramid` configures the -:term:`WebOb` request machinery to attempt to decode form submission values -into Unicode from UTF-8 implicitly. This implicit decoding happens when view -code obtains form field values via the ``request.params``, ``request.GET``, -or ``request.POST`` APIs (see :ref:`request_module` for details about these -APIs). +and handling form submission data is a property of the request implementation. +Understanding WebOb's request API is the key to understanding how to process +form submission data. + +There are some defaults that you need to be aware of when trying to handle form +submission data in a :app:`Pyramid` view. Having high-order (i.e., non-ASCII) +characters in data contained within form submissions is exceedingly common, and +the UTF-8 encoding is the most common encoding used on the web for character +data. Since Unicode values are much saner than working with and storing +bytestrings, :app:`Pyramid` configures the :term:`WebOb` request machinery to +attempt to decode form submission values into Unicode from UTF-8 implicitly. +This implicit decoding happens when view code obtains form field values via the +``request.params``, ``request.GET``, or ``request.POST`` APIs (see +:ref:`request_module` for details about these APIs). .. note:: - Many people find the difference between Unicode and UTF-8 confusing. - Unicode is a standard for representing text that supports most of the - world's writing systems. However, there are many ways that Unicode data - can be encoded into bytes for transit and storage. UTF-8 is a specific - encoding for Unicode, that is backwards-compatible with ASCII. This makes - UTF-8 very convenient for encoding data where a large subset of that data - is ASCII characters, which is largely true on the web. UTF-8 is also the - standard character encoding for URLs. + Many people find the difference between Unicode and UTF-8 confusing. Unicode + is a standard for representing text that supports most of the world's + writing systems. However, there are many ways that Unicode data can be + encoded into bytes for transit and storage. UTF-8 is a specific encoding for + Unicode that is backwards-compatible with ASCII. This makes UTF-8 very + convenient for encoding data where a large subset of that data is ASCII + characters, which is largely true on the web. UTF-8 is also the standard + character encoding for URLs. As an example, let's assume that the following form page is served up to a browser client, and its ``action`` points at some :app:`Pyramid` view code: @@ -474,8 +446,8 @@ browser client, and its ``action`` points at some :app:`Pyramid` view code: The ``myview`` view code in the :app:`Pyramid` application *must* expect that the values returned by ``request.params`` will be of type ``unicode``, as -opposed to type ``str``. The following will work to accept a form post from -the above form: +opposed to type ``str``. The following will work to accept a form post from the +above form: .. code-block:: python :linenos: @@ -503,30 +475,123 @@ encoding of UTF-8. This can be done via a response that has a with a ``meta http-equiv`` tag that implies that the charset is UTF-8 within the HTML ``head`` of the page containing the form. This must be done explicitly because all known browser clients assume that they should encode -form data in the same character set implied by ``Content-Type`` value of the -response containing the form when subsequently submitting that form. There is -no other generally accepted way to tell browser clients which charset to use -to encode form data. If you do not specify an encoding explicitly, the -browser client will choose to encode form data in its default character set -before submitting it, which may not be UTF-8 as the server expects. If a -request containing form data encoded in a non-UTF8 charset is handled by your -view code, eventually the request code accessed within your view will throw -an error when it can't decode some high-order character encoded in another -character set within form data, e.g., when ``request.params['somename']`` is -accessed. +form data in the same character set implied by the ``Content-Type`` value of +the response containing the form when subsequently submitting that form. There +is no other generally accepted way to tell browser clients which charset to use +to encode form data. If you do not specify an encoding explicitly, the browser +client will choose to encode form data in its default character set before +submitting it, which may not be UTF-8 as the server expects. If a request +containing form data encoded in a non-UTF-8 ``charset`` is handled by your view +code, eventually the request code accessed within your view will throw an error +when it can't decode some high-order character encoded in another character set +within form data, e.g., when ``request.params['somename']`` is accessed. If you are using the :class:`~pyramid.response.Response` class to generate a response, or if you use the ``render_template_*`` templating APIs, the UTF-8 -charset is set automatically as the default via the ``Content-Type`` header. -If you return a ``Content-Type`` header without an explicit charset, a -request will add a ``;charset=utf-8`` trailer to the ``Content-Type`` header -value for you, for response content types that are textual -(e.g. ``text/html``, ``application/xml``, etc) as it is rendered. If you are -using your own response object, you will need to ensure you do this yourself. +``charset`` is set automatically as the default via the ``Content-Type`` +header. If you return a ``Content-Type`` header without an explicit +``charset``, a request will add a ``;charset=utf-8`` trailer to the +``Content-Type`` header value for you for response content types that are +textual (e.g., ``text/html`` or ``application/xml``) as it is rendered. If you +are using your own response object, you will need to ensure you do this +yourself. + +.. note:: Only the *values* of request params obtained via ``request.params``, + ``request.GET`` or ``request.POST`` are decoded to Unicode objects + implicitly in the :app:`Pyramid` default configuration. The keys are still + (byte) strings. + + +.. index:: + single: view calling convention + +.. _request_and_context_view_definitions: + +Alternate View Callable Argument/Calling Conventions +---------------------------------------------------- + +Usually view callables are defined to accept only a single argument: +``request``. However, view callables may alternately be defined as classes, +functions, or any callable that accept *two* positional arguments: a +:term:`context` resource as the first argument and a :term:`request` as the +second argument. + +The :term:`context` and :term:`request` arguments passed to a view function +defined in this style can be defined as follows: + +context + The :term:`resource` object found via tree :term:`traversal` or :term:`URL + dispatch`. + +request + A :app:`Pyramid` Request object representing the current WSGI request. + +The following types work as view callables in this style: + +#. Functions that accept two arguments: ``context`` and ``request``, e.g.: + + .. code-block:: python + :linenos: + + from pyramid.response import Response + + def view(context, request): + return Response('OK') + +#. Classes that have an ``__init__`` method that accepts ``context, request``, + and a ``__call__`` method which accepts no arguments, e.g.: + + .. code-block:: python + :linenos: -.. note:: Only the *values* of request params obtained via - ``request.params``, ``request.GET`` or ``request.POST`` are decoded - to Unicode objects implicitly in the :app:`Pyramid` default - configuration. The keys are still (byte) strings. + from pyramid.response import Response + class view(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return Response('OK') + +#. Arbitrary callables that have a ``__call__`` method that accepts ``context, + request``, e.g.: + + .. code-block:: python + :linenos: + + from pyramid.response import Response + + class View(object): + def __call__(self, context, request): + return Response('OK') + view = View() # this is the view callable + +This style of calling convention is most useful for :term:`traversal` based +applications, where the context object is frequently used within the view +callable code itself. + +No matter which view calling convention is used, the view code always has +access to the context via ``request.context``. + +.. index:: + single: Passing in configuration variables + +.. _passing_in_config_variables: + +Passing Configuration Variables to a View +----------------------------------------- + +For information on passing a variable from the configuration .ini files to a +view, see :ref:`deployment_settings`. + +.. index:: + single: Pylons-style controller dispatch + +Pylons-1.0-Style "Controller" Dispatch +-------------------------------------- +A package named :term:`pyramid_handlers` (available from PyPI) provides an +analogue of :term:`Pylons`-style "controllers", which are a special kind of +view class which provides more automation when your application uses :term:`URL +dispatch` solely. diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 072ca1c74..cfcf532bc 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -10,33 +10,32 @@ Request and Response Objects .. note:: This chapter is adapted from a portion of the :term:`WebOb` documentation, originally written by Ian Bicking. -:app:`Pyramid` uses the :term:`WebOb` package to supply +:app:`Pyramid` uses the :term:`WebOb` package as a basis for its :term:`request` and :term:`response` object implementations. The -:term:`request` object that is passed to a :app:`Pyramid` -:term:`view` is an instance of the :class:`pyramid.request.Request` -class, which is a subclass of :class:`webob.Request`. The -:term:`response` returned from a :app:`Pyramid` :term:`view` -:term:`renderer` is an instance of the :mod:`webob.Response` class. -Users can also return an instance of :mod:`webob.Response` directly -from a view as necessary. - -WebOb is a project separate from :app:`Pyramid` with a separate set of -authors and a fully separate `set of documentation -<http://pythonpaste.org/webob/>`_. Pyramid adds some functionality to the -standard WebOb request, which is documented in the :ref:`request_module` API -documentation. - -WebOb provides objects for HTTP requests and responses. Specifically -it does this by wrapping the `WSGI <http://wsgi.org>`_ request -environment and response status/headers/app_iter (body). - -WebOb request and response objects provide many conveniences for -parsing WSGI requests and forming WSGI responses. WebOb is a nice way -to represent "raw" WSGI requests and responses; however, we won't -cover that use case in this document, as users of :app:`Pyramid` -don't typically need to use the WSGI-related features of WebOb -directly. The `reference documentation -<http://pythonpaste.org/webob/reference.html>`_ shows many examples of +:term:`request` object that is passed to a :app:`Pyramid` :term:`view` is an +instance of the :class:`pyramid.request.Request` class, which is a subclass of +:class:`webob.Request`. The :term:`response` returned from a :app:`Pyramid` +:term:`view` :term:`renderer` is an instance of the +:mod:`pyramid.response.Response` class, which is a subclass of the +:class:`webob.Response` class. Users can also return an instance of +:class:`pyramid.response.Response` directly from a view as necessary. + +WebOb is a project separate from :app:`Pyramid` with a separate set of authors +and a fully separate `set of documentation +<http://docs.webob.org/en/latest/index.html>`_. :app:`Pyramid` adds some +functionality to the standard WebOb request, which is documented in the +:ref:`request_module` API documentation. + +WebOb provides objects for HTTP requests and responses. Specifically it does +this by wrapping the `WSGI <http://wsgi.org>`_ request environment and response +status, header list, and app_iter (body) values. + +WebOb request and response objects provide many conveniences for parsing WSGI +requests and forming WSGI responses. WebOb is a nice way to represent "raw" +WSGI requests and responses. However, we won't cover that use case in this +document, as users of :app:`Pyramid` don't typically need to use the +WSGI-related features of WebOb directly. The `reference documentation +<http://docs.webob.org/en/latest/reference.html>`_ shows many examples of creating requests and using response objects in this manner, however. .. index:: @@ -48,60 +47,58 @@ Request The request object is a wrapper around the `WSGI environ dictionary <http://www.python.org/dev/peps/pep-0333/#environ-variables>`_. This -dictionary contains keys for each header, keys that describe the -request (including the path and query string), a file-like object for -the request body, and a variety of custom keys. You can always access -the environ with ``req.environ``. - -Some of the most important/interesting attributes of a request -object: - -``req.method``: - The request method, e.g., ``'GET'``, ``'POST'`` - -``req.GET``: - A :term:`multidict` with all the variables in the query - string. - -``req.POST``: - A :term:`multidict` with all the variables in the request - body. This only has variables if the request was a ``POST`` and - it is a form submission. - -``req.params``: - A :term:`multidict` with a combination of everything in - ``req.GET`` and ``req.POST``. - -``req.body``: - The contents of the body of the request. This contains the entire - request body as a string. This is useful when the request is a - ``POST`` that is *not* a form submission, or a request like a - ``PUT``. You can also get ``req.body_file`` for a file-like - object. - -``req.cookies``: +dictionary contains keys for each header, keys that describe the request +(including the path and query string), a file-like object for the request body, +and a variety of custom keys. You can always access the environ with +``req.environ``. + +Some of the most important and interesting attributes of a request object are +below. + +``req.method`` + The request method, e.g., ``GET``, ``POST`` + +``req.GET`` + A :term:`multidict` with all the variables in the query string. + +``req.POST`` + A :term:`multidict` with all the variables in the request body. This only + has variables if the request was a ``POST`` and it is a form submission. + +``req.params`` + A :term:`multidict` with a combination of everything in ``req.GET`` and + ``req.POST``. + +``req.body`` + The contents of the body of the request. This contains the entire request + body as a string. This is useful when the request is a ``POST`` that is + *not* a form submission, or a request like a ``PUT``. You can also get + ``req.body_file`` for a file-like object. + +``req.json_body`` + The JSON-decoded contents of the body of the request. See + :ref:`request_json_body`. + +``req.cookies`` A simple dictionary of all the cookies. -``req.headers``: +``req.headers`` A dictionary of all the headers. This dictionary is case-insensitive. -``req.urlvars`` and ``req.urlargs``: - ``req.urlvars`` are the keyword parameters associated with the - request URL. ``req.urlargs`` are the positional parameters. - These are set by products like `Routes - <http://routes.groovie.org/>`_ and `Selector - <http://lukearno.com/projects/selector/>`_. +``req.urlvars`` and ``req.urlargs`` + ``req.urlvars`` are the keyword parameters associated with the request URL. + ``req.urlargs`` are the positional parameters. These are set by products + like `Routes <http://routes.readthedocs.org/en/latest/>`_ and `Selector + <https://github.com/lukearno/selector>`_. -Also, for standard HTTP request headers there are usually attributes, -for instance: ``req.accept_language``, ``req.content_length``, -``req.user_agent``, as an example. These properties expose the -*parsed* form of each header, for whatever parsing makes sense. For -instance, ``req.if_modified_since`` returns a `datetime -<http://python.org/doc/current/lib/datetime-datetime.html>`_ object -(or None if the header is was not provided). +Also for standard HTTP request headers, there are usually attributes such as +``req.accept_language``, ``req.content_length``, and ``req.user_agent``. These +properties expose the *parsed* form of each header, for whatever parsing makes +sense. For instance, ``req.if_modified_since`` returns a :mod:`datetime` +object (or None if the header is was not provided). -.. note:: Full API documentation for the :app:`Pyramid` request - object is available in :ref:`request_module`. +.. note:: Full API documentation for the :app:`Pyramid` request object is + available in :ref:`request_module`. .. index:: single: request attributes (special) @@ -109,14 +106,14 @@ instance, ``req.if_modified_since`` returns a `datetime .. _special_request_attributes: Special Attributes Added to the Request by :app:`Pyramid` -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ In addition to the standard :term:`WebOb` attributes, :app:`Pyramid` adds special attributes to every request: ``context``, ``registry``, ``root``, ``subpath``, ``traversed``, ``view_name``, ``virtual_root``, -``virtual_root_path``, ``session``, and ``tmpl_context``, ``matchdict``, and -``matched_route``. These attributes are documented further within the -:class:`pyramid.request.Request` API documentation. +``virtual_root_path``, ``session``, ``matchdict``, and ``matched_route``. These +attributes are documented further within the :class:`pyramid.request.Request` +API documentation. .. index:: single: request URLs @@ -124,45 +121,43 @@ special attributes to every request: ``context``, ``registry``, ``root``, URLs ++++ -In addition to these attributes, there are several ways to get the URL -of the request. I'll show various values for an example URL +In addition to these attributes, there are several ways to get the URL of the +request and its parts. We'll show various values for an example URL ``http://localhost/app/blog?id=10``, where the application is mounted at ``http://localhost/app``. -``req.url``: - The full request URL, with query string, e.g., +``req.url`` + The full request URL with query string, e.g., ``http://localhost/app/blog?id=10`` -``req.host``: - The host information in the URL, e.g., - ``localhost`` +``req.host`` + The host information in the URL, e.g., ``localhost`` -``req.host_url``: +``req.host_url`` The URL with the host, e.g., ``http://localhost`` -``req.application_url``: - The URL of the application (just the SCRIPT_NAME portion of the - path, not PATH_INFO). E.g., ``http://localhost/app`` +``req.application_url`` + The URL of the application (just the ``SCRIPT_NAME`` portion of the path, + not ``PATH_INFO``), e.g., ``http://localhost/app`` -``req.path_url``: - The URL of the application including the PATH_INFO. e.g., +``req.path_url`` + The URL of the application including the ``PATH_INFO``, e.g., ``http://localhost/app/blog`` -``req.path``: - The URL including PATH_INFO without the host or scheme. e.g., +``req.path`` + The URL including ``PATH_INFO`` without the host or scheme, e.g., ``/app/blog`` -``req.path_qs``: - The URL including PATH_INFO and the query string. e.g, +``req.path_qs`` + The URL including ``PATH_INFO`` and the query string, e.g, ``/app/blog?id=10`` -``req.query_string``: - The query string in the URL, e.g., - ``id=10`` +``req.query_string`` + The query string in the URL, e.g., ``id=10`` -``req.relative_url(url, to_application=False)``: - Gives a URL, relative to the current URL. If ``to_application`` - is True, then resolves it relative to ``req.application_url``. +``req.relative_url(url, to_application=False)`` + Gives a URL relative to the current URL. If ``to_application`` is True, + then resolves it relative to ``req.application_url``. .. index:: single: request methods @@ -170,43 +165,38 @@ of the request. I'll show various values for an example URL Methods +++++++ -There are `several methods -<http://pythonpaste.org/webob/class-webob.Request.html#__init__>`_ but -only a few you'll use often: +There are methods of request objects documented in +:class:`pyramid.request.Request` but you'll find that you won't use very many +of them. Here are a couple that might be useful: -``Request.blank(base_url)``: - Creates a new request with blank information, based at the given - URL. This can be useful for subrequests and artificial requests. - You can also use ``req.copy()`` to copy an existing request, or - for subrequests ``req.copy_get()`` which copies the request but - always turns it into a GET (which is safer to share for - subrequests). +``Request.blank(base_url)`` + Creates a new request with blank information, based at the given URL. This + can be useful for subrequests and artificial requests. You can also use + ``req.copy()`` to copy an existing request, or for subrequests + ``req.copy_get()`` which copies the request but always turns it into a GET + (which is safer to share for subrequests). -``req.get_response(wsgi_application)``: - This method calls the given WSGI application with this request, - and returns a `Response`_ object. You can also use this for - subrequests, or testing. +``req.get_response(wsgi_application)`` + This method calls the given WSGI application with this request, and returns + a :class:`pyramid.response.Response` object. You can also use this for + subrequests or testing. .. index:: - single: request (and unicode) - single: unicode (and the request) + single: request (and text/unicode) + single: unicode and text (and the request) -Unicode -+++++++ +Text (Unicode) +++++++++++++++ -Many of the properties in the request object will return unicode -values if the request encoding/charset is provided. The client *can* +Many of the properties of the request object will be text values (``unicode`` +under Python 2 or ``str`` under Python 3) if the request encoding/charset is +provided. If it is provided, the values in ``req.POST``, ``req.GET``, +``req.params``, and ``req.cookies`` will contain text. The client *can* indicate the charset with something like ``Content-Type: -application/x-www-form-urlencoded; charset=utf8``, but browsers seldom -set this. You can set the charset with ``req.charset = 'utf8'``, or -during instantiation with ``Request(environ, charset='utf8')``. If -you subclass ``Request`` you can also set ``charset`` as a class-level -attribute. - -If it is set, then ``req.POST``, ``req.GET``, ``req.params``, and -``req.cookies`` will contain unicode strings. Each has a -corresponding ``req.str_*`` (e.g., ``req.str_POST``) that is always -a ``str``, and never unicode. +application/x-www-form-urlencoded; charset=utf8``, but browsers seldom set +this. You can reset the charset of an existing request with ``newreq = +req.decode('utf-8')``, or during instantiation with ``Request(environ, +charset='utf8')``. .. index:: single: multidict (WebOb) @@ -216,41 +206,160 @@ a ``str``, and never unicode. Multidict +++++++++ -Several attributes of a WebOb request are "multidict"; structures (such as +Several attributes of a WebOb request are multidict structures (such as ``request.GET``, ``request.POST``, and ``request.params``). A multidict is a -dictionary where a key can have multiple values. The quintessential example -is a query string like ``?pref=red&pref=blue``; the ``pref`` variable has two +dictionary where a key can have multiple values. The quintessential example is +a query string like ``?pref=red&pref=blue``; the ``pref`` variable has two values: ``red`` and ``blue``. -In a multidict, when you do ``request.GET['pref']`` you'll get back -only ``'blue'`` (the last value of ``pref``). Sometimes returning a -string, and sometimes returning a list, is the cause of frequent -exceptions. If you want *all* the values back, use -``request.GET.getall('pref')``. If you want to be sure there is *one -and only one* value, use ``request.GET.getone('pref')``, which will -raise an exception if there is zero or more than one value for -``pref``. - -When you use operations like ``request.GET.items()`` you'll get back -something like ``[('pref', 'red'), ('pref', 'blue')]``. All the -key/value pairs will show up. Similarly ``request.GET.keys()`` -returns ``['pref', 'pref']``. Multidict is a view on a list of -tuples; all the keys are ordered, and all the values are ordered. +In a multidict, when you do ``request.GET['pref']``, you'll get back only +``"blue"`` (the last value of ``pref``). This returned result might not be +expected—sometimes returning a string, and sometimes returning a list—and may +be cause of frequent exceptions. If you want *all* the values back, use +``request.GET.getall('pref')``. If you want to be sure there is *one and only +one* value, use ``request.GET.getone('pref')``, which will raise an exception +if there is zero or more than one value for ``pref``. + +When you use operations like ``request.GET.items()``, you'll get back something +like ``[('pref', 'red'), ('pref', 'blue')]``. All the key/value pairs will +show up. Similarly ``request.GET.keys()`` returns ``['pref', 'pref']``. +Multidict is a view on a list of tuples; all the keys are ordered, and all the +values are ordered. API documentation for a multidict exists as :class:`pyramid.interfaces.IMultiDict`. +.. index:: + pair: json_body; request + +.. _request_json_body: + +Dealing with a JSON-Encoded Request Body +++++++++++++++++++++++++++++++++++++++++ + +.. versionadded:: 1.1 + +:attr:`pyramid.request.Request.json_body` is a property that returns a +:term:`JSON`-decoded representation of the request body. If the request does +not have a body, or the body is not a properly JSON-encoded value, an exception +will be raised when this attribute is accessed. + +This attribute is useful when you invoke a :app:`Pyramid` view callable via, +for example, jQuery's ``$.ajax`` function, which has the potential to send a +request with a JSON-encoded body. + +Using ``request.json_body`` is equivalent to: + +.. code-block:: python + + from json import loads + loads(request.body, encoding=request.charset) + +Here's how to construct an AJAX request in JavaScript using :term:`jQuery` that +allows you to use the ``request.json_body`` attribute when the request is sent +to a :app:`Pyramid` application: + +.. code-block:: javascript + + jQuery.ajax({type:'POST', + url: 'http://localhost:6543/', // the pyramid server + data: JSON.stringify({'a':1}), + contentType: 'application/json; charset=utf-8'}); + +When such a request reaches a view in your application, the +``request.json_body`` attribute will be available in the view callable body. + +.. code-block:: python + + @view_config(renderer='string') + def aview(request): + print(request.json_body) + return 'OK' + +For the above view, printed to the console will be: + +.. code-block:: python + + {u'a': 1} + +For bonus points, here's a bit of client-side code that will produce a request +that has a body suitable for reading via ``request.json_body`` using Python's +``urllib2`` instead of a JavaScript AJAX request: + +.. code-block:: python + + import urllib2 + import json + + json_payload = json.dumps({'a':1}) + headers = {'Content-Type':'application/json; charset=utf-8'} + req = urllib2.Request('http://localhost:6543/', json_payload, headers) + resp = urllib2.urlopen(req) + +If you are doing Cross-origin resource sharing (CORS), then the standard +requires the browser to do a pre-flight HTTP OPTIONS request. The easiest way +to handle this is to add an extra ``view_config`` for the same route, with +``request_method`` set to ``OPTIONS``, and set the desired response header +before returning. You can find examples of response headers `Access control +CORS, Preflighted requests +<https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests>`_. + +.. index:: + single: cleaning up after request + +.. _cleaning_up_after_a_request: + +Cleaning up after a Request ++++++++++++++++++++++++++++ + +Sometimes it's required to perform some cleanup at the end of a request when a +database connection is involved. + +For example, let's say you have a ``mypackage`` :app:`Pyramid` application +package that uses SQLAlchemy, and you'd like the current SQLAlchemy database +session to be removed after each request. Put the following in the +``mypackage.__init__`` module: + +.. code-block:: python + :linenos: + + from mypackage.models import DBSession + + from pyramid.events import subscriber + from pyramid.events import NewRequest + + def cleanup_callback(request): + DBSession.remove() + + @subscriber(NewRequest) + def add_cleanup_callback(event): + event.request.add_finished_callback(cleanup_callback) + +Registering the ``cleanup_callback`` finished callback at the start of a +request (by causing the ``add_cleanup_callback`` to receive a +:class:`pyramid.events.NewRequest` event at the start of each request) will +cause the DBSession to be removed whenever request processing has ended. Note +that in the example above, for the :class:`pyramid.events.subscriber` decorator +to work, the :meth:`pyramid.config.Configurator.scan` method must be called +against your ``mypackage`` package during application initialization. + +.. note:: + This is only an example. In particular, it is not necessary to cause + ``DBSession.remove`` to be called in an application generated from any + :app:`Pyramid` scaffold, because these all use the ``pyramid_tm`` package. + The cleanup done by ``DBSession.remove`` is unnecessary when ``pyramid_tm`` + :term:`middleware` is configured into the application. + More Details ++++++++++++ -More detail about the request object API is available in: +More detail about the request object API is available as follows. -- The :class:`pyramid.request.Request` API documentation. +- :class:`pyramid.request.Request` API documentation -- The `WebOb documentation <http://pythonpaste.org/webob>`_. All - methods and attributes of a ``webob.Request`` documented within the - WebOb documentation will work with request objects created by - :app:`Pyramid`. +- `WebOb documentation <http://docs.webob.org/en/latest/index.html>`_. All + methods and attributes of a ``webob.Request`` documented within the WebOb + documentation will work with request objects created by :app:`Pyramid`. .. index:: single: response object @@ -259,65 +368,64 @@ Response ~~~~~~~~ The :app:`Pyramid` response object can be imported as -:class:`pyramid.response.Response`. This import location is merely a facade -for its original location: ``webob.Response``. +:class:`pyramid.response.Response`. This class is a subclass of the +``webob.Response`` class. The subclass does not add or change any +functionality, so the WebOb Response documentation will be completely relevant +for this class as well. A response object has three fundamental parts: -``response.status``: - The response code plus reason message, like ``'200 OK'``. To set - the code without a message, use ``status_int``, i.e.: - ``response.status_int = 200``. +``response.status`` + The response code plus reason message, like ``200 OK``. To set the code + without a message, use ``status_int``, i.e., ``response.status_int = 200``. -``response.headerlist``: - A list of all the headers, like ``[('Content-Type', - 'text/html')]``. There's a case-insensitive :term:`multidict` - in ``response.headers`` that also allows you to access - these same headers. +``response.headerlist`` + A list of all the headers, like ``[('Content-Type', 'text/html')]``. + There's a case-insensitive :term:`multidict` in ``response.headers`` that + also allows you to access these same headers. -``response.app_iter``: - An iterable (such as a list or generator) that will produce the - content of the response. This is also accessible as - ``response.body`` (a string), ``response.unicode_body`` (a - unicode object, informed by ``response.charset``), and - ``response.body_file`` (a file-like object; writing to it appends - to ``app_iter``). +``response.app_iter`` + An iterable (such as a list or generator) that will produce the content of + the response. This is also accessible as ``response.body`` (a string), + ``response.text`` (a unicode object, informed by ``response.charset``), and + ``response.body_file`` (a file-like object; writing to it appends to + ``app_iter``). -Everything else in the object derives from this underlying state. -Here's the highlights: +Everything else in the object typically derives from this underlying state. +Here are some highlights: ``response.content_type`` The content type *not* including the ``charset`` parameter. + Typical use: ``response.content_type = 'text/html'``. -``response.charset``: - The ``charset`` parameter of the content-type, it also informs - encoding in ``response.unicode_body``. - ``response.content_type_params`` is a dictionary of all the - parameters. - -``response.set_cookie(key, value, max_age=None, path='/', ...)``: - Set a cookie. The keyword arguments control the various cookie - parameters. The ``max_age`` argument is the length for the cookie - to live in seconds (you may also use a timedelta object). The - ``Expires`` key will also be set based on the value of - ``max_age``. - -``response.delete_cookie(key, path='/', domain=None)``: - Delete a cookie from the client. This sets ``max_age`` to 0 and - the cookie value to ``''``. - -``response.cache_expires(seconds=0)``: - This makes this response cacheable for the given number of seconds, - or if ``seconds`` is 0 then the response is uncacheable (this also - sets the ``Expires`` header). - -``response(environ, start_response)``: - The response object is a WSGI application. As an application, it - acts according to how you create it. It *can* do conditional - responses if you pass ``conditional_response=True`` when - instantiating (or set that attribute later). It can also do HEAD - and Range requests. + Default value: ``response.content_type = 'text/html'``. + +``response.charset`` + The ``charset`` parameter of the content-type, it also informs encoding in + ``response.text``. ``response.content_type_params`` is a dictionary of all + the parameters. + +``response.set_cookie(key, value, max_age=None, path='/', ...)`` + Set a cookie. The keyword arguments control the various cookie parameters. + The ``max_age`` argument is the length for the cookie to live in seconds + (you may also use a timedelta object). The ``Expires`` key will also be + set based on the value of ``max_age``. + +``response.delete_cookie(key, path='/', domain=None)`` + Delete a cookie from the client. This sets ``max_age`` to 0 and the cookie + value to ``''``. + +``response.cache_expires(seconds=0)`` + This makes the response cacheable for the given number of seconds, or if + ``seconds`` is ``0`` then the response is uncacheable (this also sets the + ``Expires`` header). + +``response(environ, start_response)`` + The response object is a WSGI application. As an application, it acts + according to how you create it. It *can* do conditional responses if you + pass ``conditional_response=True`` when instantiating (or set that + attribute later). It can also do HEAD and Range requests. .. index:: single: response headers @@ -325,12 +433,11 @@ Here's the highlights: Headers +++++++ -Like the request, most HTTP response headers are available as -properties. These are parsed, so you can do things like -``response.last_modified = os.path.getmtime(filename)``. +Like the request, most HTTP response headers are available as properties. These +are parsed, so you can do things like ``response.last_modified = +os.path.getmtime(filename)``. -The details are available in the `extracted Response documentation -<http://pythonpaste.org/webob/class-webob.Response.html>`_. +The details are available in the :mod:`webob.response` API documentation. .. index:: single: response (creating) @@ -338,9 +445,9 @@ The details are available in the `extracted Response documentation Instantiating the Response ++++++++++++++++++++++++++ -Of course most of the time you just want to *make* a response. -Generally any attribute of the response can be passed in as a keyword -argument to the class; e.g.: +Of course most of the time you just want to *make* a response. Generally any +attribute of the response can be passed in as a keyword argument to the class, +e.g.: .. code-block:: python :linenos: @@ -348,29 +455,31 @@ argument to the class; e.g.: from pyramid.response import Response response = Response(body='hello world!', content_type='text/plain') -The status defaults to ``'200 OK'``. The content_type does not default to -anything, though if you subclass :class:`pyramid.response.Response` and set -``default_content_type`` you can override this behavior. +The status defaults to ``'200 OK'``. + +The value of ``content_type`` defaults to +``webob.response.Response.default_content_type``, which is ``text/html``. You +can subclass :class:`pyramid.response.Response` and set +``default_content_type`` to override this behavior. .. index:: - single: response exceptions + single: exception responses Exception Responses +++++++++++++++++++ To facilitate error responses like ``404 Not Found``, the module -:mod:`webob.exc` contains classes for each kind of error response. These -include boring, but appropriate error bodies. The exceptions exposed by this -module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` "facade" module. This import location is merely -a facade for the original location of these exceptions: ``webob.exc``. +:mod:`pyramid.httpexceptions` contains classes for each kind of error response. +These include boring but appropriate error bodies. The exceptions exposed by +this module, when used under :app:`Pyramid`, should be imported from the +:mod:`pyramid.httpexceptions` module. This import location contains subclasses +and replacements that mirror those in the ``webob.exc`` module. Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason -for the error. For instance, :class:`pyramid.httpexceptions.HTTPNotFound`. It -subclasses :class:`pyramid.Response`, so you can manipulate the instances in -the same way. A typical example is: +for the error. For instance, :class:`pyramid.httpexceptions.HTTPNotFound` +subclasses :class:`pyramid.response.Response`, so you can manipulate the +instances in the same way. A typical example is: -.. ignore-next-block .. code-block:: python :linenos: @@ -381,33 +490,10 @@ the same way. A typical example is: # or: response = HTTPMovedPermanently(location=new_url) -These are not exceptions unless you are using Python 2.5+, because -they are new-style classes which are not allowed as exceptions until -Python 2.5. To get an exception object use ``response.exception``. -You can use this like: - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import HTTPNotFound - - def aview(request): - try: - # ... stuff ... - raise HTTPNotFound('No such resource').exception - except HTTPException, e: - return request.get_response(e) - -The exceptions are still WSGI applications, but you cannot set -attributes like ``content_type``, ``charset``, etc. on these exception -objects. - More Details ++++++++++++ More details about the response object API are available in the :mod:`pyramid.response` documentation. More details about exception responses are in the :mod:`pyramid.httpexceptions` API documentation. The `WebOb -documentation <http://pythonpaste.org/webob>`_ is also useful. - +documentation <http://docs.webob.org/en/latest/index.html>`_ is also useful. diff --git a/docs/narr/zca.rst b/docs/narr/zca.rst index a99fd8b24..784886563 100644 --- a/docs/narr/zca.rst +++ b/docs/narr/zca.rst @@ -9,19 +9,17 @@ .. _zca_chapter: Using the Zope Component Architecture in :app:`Pyramid` -========================================================== +======================================================= -Under the hood, :app:`Pyramid` uses a :term:`Zope Component -Architecture` component registry as its :term:`application registry`. -The Zope Component Architecture is referred to colloquially as the -"ZCA." +Under the hood, :app:`Pyramid` uses a :term:`Zope Component Architecture` +component registry as its :term:`application registry`. The Zope Component +Architecture is referred to colloquially as the "ZCA." The ``zope.component`` API used to access data in a traditional Zope -application can be opaque. For example, here is a typical "unnamed -utility" lookup using the :func:`zope.component.getUtility` global API -as it might appear in a traditional Zope application: +application can be opaque. For example, here is a typical "unnamed utility" +lookup using the :func:`zope.component.getUtility` global API as it might +appear in a traditional Zope application: -.. ignore-next-block .. code-block:: python :linenos: @@ -29,23 +27,21 @@ as it might appear in a traditional Zope application: from zope.component import getUtility settings = getUtility(ISettings) -After this code runs, ``settings`` will be a Python dictionary. But -it's unlikely that any "civilian" will be able to figure this out just -by reading the code casually. When the ``zope.component.getUtility`` -API is used by a developer, the conceptual load on a casual reader of -code is high. +After this code runs, ``settings`` will be a Python dictionary. But it's +unlikely that any "civilian" will be able to figure this out just by reading +the code casually. When the ``zope.component.getUtility`` API is used by a +developer, the conceptual load on a casual reader of code is high. -While the ZCA is an excellent tool with which to build a *framework* -such as :app:`Pyramid`, it is not always the best tool with which -to build an *application* due to the opacity of the ``zope.component`` -APIs. Accordingly, :app:`Pyramid` tends to hide the presence of the -ZCA from application developers. You needn't understand the ZCA to -create a :app:`Pyramid` application; its use is effectively only a -framework implementation detail. +While the ZCA is an excellent tool with which to build a *framework* such as +:app:`Pyramid`, it is not always the best tool with which to build an +*application* due to the opacity of the ``zope.component`` APIs. Accordingly, +:app:`Pyramid` tends to hide the presence of the ZCA from application +developers. You needn't understand the ZCA to create a :app:`Pyramid` +application; its use is effectively only a framework implementation detail. -However, developers who are already used to writing :term:`Zope` -applications often still wish to use the ZCA while building a -:app:`Pyramid` application; :mod:`pyramid` makes this possible. +However, developers who are already used to writing :term:`Zope` applications +often still wish to use the ZCA while building a :app:`Pyramid` application. +:app:`Pyramid` makes this possible. .. index:: single: get_current_registry @@ -53,89 +49,81 @@ applications often still wish to use the ZCA while building a single: getSiteManager single: ZCA global API -Using the ZCA Global API in a :app:`Pyramid` Application ------------------------------------------------------------ - -:term:`Zope` uses a single ZCA registry -- the "global" ZCA registry --- for all Zope applications that run in the same Python process, -effectively making it impossible to run more than one Zope application -in a single process. - -However, for ease of deployment, it's often useful to be able to run -more than a single application per process. For example, use of a -:term:`Paste` "composite" allows you to run separate individual WSGI -applications in the same process, each answering requests for some URL -prefix. This makes it possible to run, for example, a TurboGears -application at ``/turbogears`` and a :app:`Pyramid` application at -``/pyramid``, both served up using the same :term:`WSGI` server -within a single Python process. - -Most production Zope applications are relatively large, making it -impractical due to memory constraints to run more than one Zope -application per Python process. However, a :app:`Pyramid` application -may be very small and consume very little memory, so it's a reasonable -goal to be able to run more than one :app:`Pyramid` application per -process. - -In order to make it possible to run more than one :app:`Pyramid` -application in a single process, :app:`Pyramid` defaults to using a -separate ZCA registry *per application*. - -While this services a reasonable goal, it causes some issues when -trying to use patterns which you might use to build a typical -:term:`Zope` application to build a :app:`Pyramid` application. -Without special help, ZCA "global" APIs such as -``zope.component.getUtility`` and ``zope.component.getSiteManager`` -will use the ZCA "global" registry. Therefore, these APIs -will appear to fail when used in a :app:`Pyramid` application, -because they'll be consulting the ZCA global registry rather than the -component registry associated with your :app:`Pyramid` application. - -There are three ways to fix this: by disusing the ZCA global API -entirely, by using -:meth:`pyramid.config.Configurator.hook_zca` or by passing -the ZCA global registry to the :term:`Configurator` constructor at -startup time. We'll describe all three methods in this section. +Using the ZCA global API in a :app:`Pyramid` application +-------------------------------------------------------- + +:term:`Zope` uses a single ZCA registry—the "global" ZCA registry—for all Zope +applications that run in the same Python process, effectively making it +impossible to run more than one Zope application in a single process. + +However, for ease of deployment, it's often useful to be able to run more than +a single application per process. For example, use of a :term:`PasteDeploy` +"composite" allows you to run separate individual WSGI applications in the same +process, each answering requests for some URL prefix. This makes it possible +to run, for example, a TurboGears application at ``/turbogears`` and a +:app:`Pyramid` application at ``/pyramid``, both served up using the same +:term:`WSGI` server within a single Python process. + +Most production Zope applications are relatively large, making it impractical +due to memory constraints to run more than one Zope application per Python +process. However, a :app:`Pyramid` application may be very small and consume +very little memory, so it's a reasonable goal to be able to run more than one +:app:`Pyramid` application per process. + +In order to make it possible to run more than one :app:`Pyramid` application in +a single process, :app:`Pyramid` defaults to using a separate ZCA registry *per +application*. + +While this services a reasonable goal, it causes some issues when trying to use +patterns which you might use to build a typical :term:`Zope` application to +build a :app:`Pyramid` application. Without special help, ZCA "global" APIs +such as :func:`zope.component.getUtility` and +:func:`zope.component.getSiteManager` will use the ZCA "global" registry. +Therefore, these APIs will appear to fail when used in a :app:`Pyramid` +application, because they'll be consulting the ZCA global registry rather than +the component registry associated with your :app:`Pyramid` application. + +There are three ways to fix this: by disusing the ZCA global API entirely, by +using :meth:`pyramid.config.Configurator.hook_zca` or by passing the ZCA global +registry to the :term:`Configurator` constructor at startup time. We'll +describe all three methods in this section. .. index:: single: request.registry .. _disusing_the_global_zca_api: -Disusing the Global ZCA API +Disusing the global ZCA API +++++++++++++++++++++++++++ ZCA "global" API functions such as ``zope.component.getSiteManager``, -``zope.component.getUtility``, ``zope.component.getAdapter``, and -``zope.component.getMultiAdapter`` aren't strictly necessary. Every -component registry has a method API that offers the same -functionality; it can be used instead. For example, presuming the -``registry`` value below is a Zope Component Architecture component -registry, the following bit of code is equivalent to -``zope.component.getUtility(IFoo)``: +``zope.component.getUtility``, :func:`zope.component.getAdapter`, and +:func:`zope.component.getMultiAdapter` aren't strictly necessary. Every +component registry has a method API that offers the same functionality; it can +be used instead. For example, presuming the ``registry`` value below is a Zope +Component Architecture component registry, the following bit of code is +equivalent to ``zope.component.getUtility(IFoo)``: .. code-block:: python - :linenos: registry.getUtility(IFoo) -The full method API is documented in the ``zope.component`` package, -but it largely mirrors the "global" API almost exactly. +The full method API is documented in the ``zope.component`` package, but it +largely mirrors the "global" API almost exactly. -If you are willing to disuse the "global" ZCA APIs and use the method -interface of a registry instead, you need only know how to obtain the -:app:`Pyramid` component registry. +If you are willing to disuse the "global" ZCA APIs and use the method interface +of a registry instead, you need only know how to obtain the :app:`Pyramid` +component registry. There are two ways of doing so: -- use the :func:`pyramid.threadlocal.get_current_registry` - function within :app:`Pyramid` view or resource code. This will - always return the "current" :app:`Pyramid` application registry. +- use the :func:`pyramid.threadlocal.get_current_registry` function within + :app:`Pyramid` view or resource code. This will always return the "current" + :app:`Pyramid` application registry. -- use the attribute of the :term:`request` object named ``registry`` - in your :app:`Pyramid` view code, eg. ``request.registry``. This - is the ZCA component registry related to the running - :app:`Pyramid` application. +- use the attribute of the :term:`request` object named ``registry`` in your + :app:`Pyramid` view code, e.g., ``request.registry``. This is the ZCA + component registry related to the running :app:`Pyramid` application. See :ref:`threadlocals_chapter` for more information about :func:`pyramid.threadlocal.get_current_registry`. @@ -145,7 +133,7 @@ See :ref:`threadlocals_chapter` for more information about .. _hook_zca: -Enabling the ZCA Global API by Using ``hook_zca`` +Enabling the ZCA global API by using ``hook_zca`` +++++++++++++++++++++++++++++++++++++++++++++++++ Consider the following bit of idiomatic :app:`Pyramid` startup code: @@ -153,7 +141,6 @@ Consider the following bit of idiomatic :app:`Pyramid` startup code: .. code-block:: python :linenos: - from zope.component import getGlobalSiteManager from pyramid.config import Configurator def app(global_settings, **settings): @@ -161,36 +148,32 @@ Consider the following bit of idiomatic :app:`Pyramid` startup code: config.include('some.other.package') return config.make_wsgi_app() -When the ``app`` function above is run, a :term:`Configurator` is -constructed. When the configurator is created, it creates a *new* -:term:`application registry` (a ZCA component registry). A new -registry is constructed whenever the ``registry`` argument is omitted -when a :term:`Configurator` constructor is called, or when a -``registry`` argument with a value of ``None`` is passed to a -:term:`Configurator` constructor. - -During a request, the application registry created by the Configurator -is "made current". This means calls to -:func:`~pyramid.threadlocal.get_current_registry` in the thread -handling the request will return the component registry associated -with the application. - -As a result, application developers can use ``get_current_registry`` -to get the registry and thus get access to utilities and such, as per -:ref:`disusing_the_global_zca_api`. But they still cannot use the -global ZCA API. Without special treatment, the ZCA global APIs will -always return the global ZCA registry (the one in -``zope.component.globalregistry.base``). - -To "fix" this and make the ZCA global APIs use the "current" -:app:`Pyramid` registry, you need to call -:meth:`~pyramid.config.Configurator.hook_zca` within your setup code. -For example: +When the ``app`` function above is run, a :term:`Configurator` is constructed. +When the configurator is created, it creates a *new* :term:`application +registry` (a ZCA component registry). A new registry is constructed whenever +the ``registry`` argument is omitted, when a :term:`Configurator` constructor +is called, or when a ``registry`` argument with a value of ``None`` is passed +to a :term:`Configurator` constructor. + +During a request, the application registry created by the Configurator is "made +current". This means calls to +:func:`~pyramid.threadlocal.get_current_registry` in the thread handling the +request will return the component registry associated with the application. + +As a result, application developers can use ``get_current_registry`` to get the +registry and thus get access to utilities and such, as per +:ref:`disusing_the_global_zca_api`. But they still cannot use the global ZCA +API. Without special treatment, the ZCA global APIs will always return the +global ZCA registry (the one in ``zope.component.globalregistry.base``). + +To "fix" this and make the ZCA global APIs use the "current" :app:`Pyramid` +registry, you need to call :meth:`~pyramid.config.Configurator.hook_zca` within +your setup code. For example: .. code-block:: python :linenos: + :emphasize-lines: 5 - from zope.component import getGlobalSiteManager from pyramid.config import Configurator def app(global_settings, **settings): @@ -199,9 +182,9 @@ For example: config.include('some.other.application') return config.make_wsgi_app() -We've added a line to our original startup code, line number 6, which -calls ``config.hook_zca()``. The effect of this line under the hood -is that an analogue of the following code is executed: +We've added a line to our original startup code, line number 5, which calls +``config.hook_zca()``. The effect of this line under the hood is that an +analogue of the following code is executed: .. code-block:: python :linenos: @@ -210,17 +193,15 @@ is that an analogue of the following code is executed: from pyramid.threadlocal import get_current_registry getSiteManager.sethook(get_current_registry) -This causes the ZCA global API to start using the :app:`Pyramid` -application registry in threads which are running a :app:`Pyramid` -request. +This causes the ZCA global API to start using the :app:`Pyramid` application +registry in threads which are running a :app:`Pyramid` request. -Calling ``hook_zca`` is usually sufficient to "fix" the problem of -being able to use the global ZCA API within a :app:`Pyramid` -application. However, it also means that a Zope application that is -running in the same process may start using the :app:`Pyramid` -global registry instead of the Zope global registry, effectively -inverting the original problem. In such a case, follow the steps in -the next section, :ref:`using_the_zca_global_registry`. +Calling ``hook_zca`` is usually sufficient to "fix" the problem of being able +to use the global ZCA API within a :app:`Pyramid` application. However, it +also means that a Zope application that is running in the same process may +start using the :app:`Pyramid` global registry instead of the Zope global +registry, effectively inverting the original problem. In such a case, follow +the steps in the next section, :ref:`using_the_zca_global_registry`. .. index:: single: get_current_registry @@ -229,14 +210,15 @@ the next section, :ref:`using_the_zca_global_registry`. .. _using_the_zca_global_registry: -Enabling the ZCA Global API by Using The ZCA Global Registry +Enabling the ZCA global API by using the ZCA global registry ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -You can tell your :app:`Pyramid` application to use the ZCA global -registry at startup time instead of constructing a new one: +You can tell your :app:`Pyramid` application to use the ZCA global registry at +startup time instead of constructing a new one: .. code-block:: python :linenos: + :emphasize-lines: 5-7 from zope.component import getGlobalSiteManager from pyramid.config import Configurator @@ -245,20 +227,17 @@ registry at startup time instead of constructing a new one: globalreg = getGlobalSiteManager() config = Configurator(registry=globalreg) config.setup_registry(settings=settings) - config.hook_zca() config.include('some.other.application') return config.make_wsgi_app() -Lines 5, 6, and 7 above are the interesting ones. Line 5 retrieves -the global ZCA component registry. Line 6 creates a -:term:`Configurator`, passing the global ZCA registry into its -constructor as the ``registry`` argument. Line 7 "sets up" the global -registry with Pyramid-specific registrations; this is code that is -normally executed when a registry is constructed rather than created, +Lines 5, 6, and 7 above are the interesting ones. Line 5 retrieves the global +ZCA component registry. Line 6 creates a :term:`Configurator`, passing the +global ZCA registry into its constructor as the ``registry`` argument. Line 7 +"sets up" the global registry with Pyramid-specific registrations; this is code +that is normally executed when a registry is constructed rather than created, but we must call it "by hand" when we pass an explicit registry. -At this point, :app:`Pyramid` will use the ZCA global registry -rather than creating a new application-specific registry; since by -default the ZCA global API will use this registry, things will work as -you might expect a Zope app to when you use the global ZCA API. - +At this point, :app:`Pyramid` will use the ZCA global registry rather than +creating a new application-specific registry. Since by default the ZCA global +API will use this registry, things will work as you might expect in a Zope app +when you use the global ZCA API. diff --git a/docs/pscripts/index.rst b/docs/pscripts/index.rst new file mode 100644 index 000000000..857e0564f --- /dev/null +++ b/docs/pscripts/index.rst @@ -0,0 +1,12 @@ +.. _pscripts_documentation: + +``p*`` Scripts Documentation +============================ + +Command line programs (``p*`` scripts) included with :app:`Pyramid`. + +.. toctree:: + :maxdepth: 1 + :glob: + + * diff --git a/docs/pscripts/pcreate.rst b/docs/pscripts/pcreate.rst new file mode 100644 index 000000000..b5ec3f4e2 --- /dev/null +++ b/docs/pscripts/pcreate.rst @@ -0,0 +1,13 @@ +.. index:: + single: pcreate; --help + +.. _pcreate_script: + +``pcreate`` +----------- + +.. program-output:: pcreate --help + :prompt: + :shell: + +.. seealso:: :ref:`creating_a_project` diff --git a/docs/pscripts/pdistreport.rst b/docs/pscripts/pdistreport.rst new file mode 100644 index 000000000..1c53fb6e9 --- /dev/null +++ b/docs/pscripts/pdistreport.rst @@ -0,0 +1,13 @@ +.. index:: + single: pdistreport; --help + +.. _pdistreport_script: + +``pdistreport`` +--------------- + +.. program-output:: pdistreport --help + :prompt: + :shell: + +.. seealso:: :ref:`showing_distributions` diff --git a/docs/pscripts/prequest.rst b/docs/pscripts/prequest.rst new file mode 100644 index 000000000..a15827767 --- /dev/null +++ b/docs/pscripts/prequest.rst @@ -0,0 +1,13 @@ +.. index:: + single: prequest; --help + +.. _prequest_script: + +``prequest`` +------------ + +.. program-output:: prequest --help + :prompt: + :shell: + +.. seealso:: :ref:`invoking_a_request` diff --git a/docs/pscripts/proutes.rst b/docs/pscripts/proutes.rst new file mode 100644 index 000000000..09ed013e1 --- /dev/null +++ b/docs/pscripts/proutes.rst @@ -0,0 +1,13 @@ +.. index:: + single: proutes; --help + +.. _proutes_script: + +``proutes`` +----------- + +.. program-output:: proutes --help + :prompt: + :shell: + +.. seealso:: :ref:`displaying_application_routes` diff --git a/docs/pscripts/pserve.rst b/docs/pscripts/pserve.rst new file mode 100644 index 000000000..d33d4a484 --- /dev/null +++ b/docs/pscripts/pserve.rst @@ -0,0 +1,13 @@ +.. index:: + single: pserve; --help + +.. _pserve_script: + +``pserve`` +---------- + +.. program-output:: pserve --help + :prompt: + :shell: + +.. seealso:: :ref:`running_the_project_application` diff --git a/docs/pscripts/pshell.rst b/docs/pscripts/pshell.rst new file mode 100644 index 000000000..cfd84d4f8 --- /dev/null +++ b/docs/pscripts/pshell.rst @@ -0,0 +1,13 @@ +.. index:: + single: pshell; --help + +.. _pshell_script: + +``pshell`` +---------- + +.. program-output:: pshell --help + :prompt: + :shell: + +.. seealso:: :ref:`interactive_shell` diff --git a/docs/pscripts/ptweens.rst b/docs/pscripts/ptweens.rst new file mode 100644 index 000000000..02e23e49a --- /dev/null +++ b/docs/pscripts/ptweens.rst @@ -0,0 +1,13 @@ +.. index:: + single: ptweens; --help + +.. _ptweens_script: + +``ptweens`` +----------- + +.. program-output:: ptweens --help + :prompt: + :shell: + +.. seealso:: :ref:`displaying_tweens` diff --git a/docs/pscripts/pviews.rst b/docs/pscripts/pviews.rst new file mode 100644 index 000000000..b4de5c054 --- /dev/null +++ b/docs/pscripts/pviews.rst @@ -0,0 +1,13 @@ +.. index:: + single: pviews; --help + +.. _pviews_script: + +``pviews`` +---------- + +.. program-output:: pviews --help + :prompt: + :shell: + +.. seealso:: :ref:`displaying_matching_views` diff --git a/docs/python-3.png b/docs/python-3.png Binary files differnew file mode 100644 index 000000000..46ecd8581 --- /dev/null +++ b/docs/python-3.png diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst new file mode 100644 index 000000000..78af6fd40 --- /dev/null +++ b/docs/quick_tour.rst @@ -0,0 +1,965 @@ +.. _quick_tour: + +===================== +Quick Tour of Pyramid +===================== + +Pyramid lets you start small and finish big. This *Quick Tour* of Pyramid is +for those who want to evaluate Pyramid, whether you are new to Python web +frameworks, or a pro in a hurry. For more detailed treatment of each topic, +give the :ref:`quick_tutorial` a try. + + +Installation +============ + +Once you have a standard Python environment setup, getting started with Pyramid +is a breeze. Unfortunately "standard" is not so simple in Python. For this +Quick Tour, it means `Python <https://www.python.org/downloads/>`_, `venv +<https://packaging.python.org/en/latest/projects/#venv>`_ (or `virtualenv for +Python 2.7 <https://packaging.python.org/en/latest/projects/#virtualenv>`_), +`pip <https://packaging.python.org/en/latest/projects/#pip>`_, and `setuptools +<https://packaging.python.org/en/latest/projects/#easy-install>`_. + +To save a little bit of typing and to be certain that we use the modules, +scripts, and packages installed in our virtual environment, we'll set an +environment variable, too. + +As an example, for Python 3.5+ on Linux: + +.. parsed-literal:: + + # set an environment variable to where you want your virtual environment + $ export VENV=~/env + # create the virtual environment + $ python3 -m venv $VENV + # install pyramid + $ $VENV/bin/pip install pyramid + # or for a specific released version + $ $VENV/bin/pip install "pyramid==\ |release|\ " + +For Windows: + +.. parsed-literal:: + + # set an environment variable to where you want your virtual environment + c:\> set VENV=c:\env + # create the virtual environment + c:\\> c:\\Python35\\python3 -m venv %VENV% + # install pyramid + c:\\> %VENV%\\Scripts\\pip install pyramid + # or for a specific released version + c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ " + +Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick +Tour*. We're showing Python 3 for simplicity. (Pyramid had production support +for Python 3 in October 2011.) Also for simplicity, the remaining examples will +show only UNIX commands. + +.. seealso:: See also: + :ref:`Quick Tutorial section on Requirements <qtut_requirements>`, + :ref:`installing_unix`, :ref:`Before You Install <installing_chapter>`, and + :ref:`Installing Pyramid on a Windows System <installing_windows>`. + + +Hello World +=========== + +Microframeworks have shown that learning starts best from a very small first +step. Here's a tiny application in Pyramid: + +.. literalinclude:: quick_tour/hello_world/app.py + :linenos: + +This simple example is easy to run. Save this as ``app.py`` and run it: + +.. code-block:: bash + + $ $VENV/bin/python ./app.py + +Next open http://localhost:6543/ in a browser, and you will see the ``Hello +World!`` message. + +New to Python web programming? If so, some lines in the module merit +explanation: + +#. *Line 10*. ``if __name__ == '__main__':`` is Python's way of saying "Start + here when running from the command line". + +#. *Lines 11-13*. Use Pyramid's :term:`configurator` to connect :term:`view` + code to a particular URL :term:`route`. + +#. *Lines 6-7*. Implement the view code that generates the :term:`response`. + +#. *Lines 14-16*. Publish a :term:`WSGI` app using an HTTP server. + +As shown in this example, the :term:`configurator` plays a central role in +Pyramid development. Building an application from loosely-coupled parts via +:doc:`../narr/configuration` is a central idea in Pyramid, one that we will +revisit regurlarly in this *Quick Tour*. + +.. seealso:: See also: + :ref:`Quick Tutorial Hello World <qtut_hello_world>`, + :ref:`firstapp_chapter`, and :ref:`Todo List Application in One File + <cookbook:single-file-tutorial>`. + + +Handling web requests and responses +=================================== + +Developing for the web means processing web requests. As this is a critical +part of a web application, web developers need a robust, mature set of software +for web requests. + +Pyramid has always fit nicely into the existing world of Python web development +(virtual environments, packaging, scaffolding, one of the first to embrace +Python 3, etc.). Pyramid turned to the well-regarded :term:`WebOb` Python +library for request and response handling. In our example above, Pyramid hands +``hello_world`` a ``request`` that is :ref:`based on WebOb <webob_chapter>`. + +Let's see some features of requests and responses in action: + +.. literalinclude:: quick_tour/requests/app.py + :pyobject: hello_world + +In this Pyramid view, we get the URL being visited from ``request.url``. Also +if you visited http://localhost:6543/?name=alice in a browser, the name is +included in the body of the response: + +.. code-block:: text + + URL http://localhost:6543/?name=alice with name: alice + +Finally we set the response's content type, and return the Response. + +.. seealso:: See also: + :ref:`Quick Tutorial Request and Response <qtut_request_response>` and + :ref:`webob_chapter`. + + +Views +===== + +For the examples above, the ``hello_world`` function is a "view". In Pyramid +views are the primary way to accept web requests and return responses. + +So far our examples place everything in one file: + +- the view function + +- its registration with the configurator + +- the route to map it to an URL + +- the WSGI application launcher + +Let's move the views out to their own ``views.py`` module and change the +``app.py`` to scan that module, looking for decorators that set up the views. + +First our revised ``app.py``: + +.. literalinclude:: quick_tour/views/app.py + :linenos: + +We added some more routes, but we also removed the view code. Our views and +their registrations (via decorators) are now in a module ``views.py``, which is +scanned via ``config.scan('views')``. + +We now have a ``views.py`` module that is focused on handling requests and +responses: + +.. literalinclude:: quick_tour/views/views.py + :linenos: + +We have four views, each leading to the other. If you start at +http://localhost:6543/, you get a response with a link to the next view. The +``hello_view`` (available at the URL ``/howdy``) has a link to the +``redirect_view``, which issues a redirect to the final view. + +Earlier we saw ``config.add_view`` as one way to configure a view. This section +introduces ``@view_config``. Pyramid's configuration supports :term:`imperative +configuration`, such as the ``config.add_view`` in the previous example. You +can also use :term:`declarative configuration` in which a Python +:term:`decorator` is placed on the line above the view. Both approaches result +in the same final configuration, thus usually it is simply a matter of taste. + +.. seealso:: See also: + :ref:`Quick Tutorial Views <qtut_views>`, :doc:`../narr/views`, + :doc:`../narr/viewconfig`, and :ref:`debugging_view_configuration`. + + +Routing +======= + +Writing web applications usually means sophisticated URL design. We just saw +some Pyramid machinery for requests and views. Let's look at features that help +with routing. + +Above we saw the basics of routing URLs to views in Pyramid: + +- Your project's "setup" code registers a route name to be used when matching + part of the URL. + +- Elsewhere a view is configured to be called for that route name. + +.. note:: + + Why do this twice? Other Python web frameworks let you create a route and + associate it with a view in one step. As illustrated in + :ref:`routes_need_ordering`, multiple routes might match the same URL + pattern. Rather than provide ways to help guess, Pyramid lets you be + explicit in ordering. Pyramid also gives facilities to avoid the problem. + +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 + :linenos: + :lines: 6 + :lineno-start: 6 + +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 + :linenos: + :lines: 5-8 + :lineno-start: 5 + :emphasize-lines: 3 + +``request.matchdict`` contains values from the URL that match the "replacement +patterns" (the curly braces) in the route declaration. This information can +then be used in your view. + +.. seealso:: See also: + :ref:`Quick Tutorial Routing <qtut_routing>`, :doc:`../narr/urldispatch`, + :ref:`debug_routematch_section`, and :doc:`../narr/router`. + + +Templating +========== + +Ouch. We have been making our own ``Response`` and filling the response body +with HTML. You usually won't embed an HTML string directly in Python, but +instead you will use a templating language. + +Pyramid doesn't mandate a particular database system, form library, and so on. +It encourages replaceability. This applies equally to templating, which is +fortunate: developers have strong views about template languages. That said, +the Pylons Project officially supports bindings for Chameleon, Jinja2, and +Mako. In this step let's use Chameleon. + +Let's add ``pyramid_chameleon``, a Pyramid :term:`add-on` which enables +Chameleon as a :term:`renderer` in our Pyramid application: + +.. code-block:: bash + + $ $VENV/bin/pip install pyramid_chameleon + +With the package installed, we can include the template bindings into our +configuration in ``app.py``: + +.. literalinclude:: quick_tour/templating/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 + +Now lets change our ``views.py`` file: + +.. literalinclude:: quick_tour/templating/views.py + :linenos: + :emphasize-lines: 4,6 + +Ahh, that looks better. We have a view that is focused on Python code. Our +``@view_config`` decorator specifies a :term:`renderer` that points to our +template file. Our view then simply returns data which is then supplied to our +template ``hello_world.pt``: + +.. literalinclude:: quick_tour/templating/hello_world.pt + :language: html + +Since our view returned ``dict(name=request.matchdict['name'])``, we can use +``name`` as a variable in our template via ``${name}``. + +.. seealso:: See also: + :ref:`Quick Tutorial Templating <qtut_templating>`, + :doc:`../narr/templates`, :ref:`debugging_templates`, and + :ref:`available_template_system_bindings`. + + +Templating with Jinja2 +====================== + +We just said Pyramid doesn't prefer one templating language over another. Time +to prove it. Jinja2 is a popular templating system, modeled after Django's +templates. Let's add ``pyramid_jinja2``, a Pyramid :term:`add-on` which enables +Jinja2 as a :term:`renderer` in our Pyramid applications: + +.. code-block:: bash + + $ $VENV/bin/pip install pyramid_jinja2 + +With the package installed, we can include the template bindings into our +configuration: + +.. literalinclude:: quick_tour/jinja2/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 + +The only change in our view is to point the renderer at the ``.jinja2`` file: + +.. literalinclude:: quick_tour/jinja2/views.py + :linenos: + :lines: 4-6 + :lineno-start: 4 + :emphasize-lines: 1 + +Our Jinja2 template is very similar to our previous template: + +.. literalinclude:: quick_tour/jinja2/hello_world.jinja2 + :language: html + +Pyramid's templating add-ons register a new kind of renderer into your +application. The renderer registration maps to different kinds of filename +extensions. In this case, changing the extension from ``.pt`` to ``.jinja2`` +passed the view response through the ``pyramid_jinja2`` renderer. + +.. seealso:: See also: + :ref:`Quick Tutorial Jinja2 <qtut_jinja2>`, `Jinja2 homepage + <http://jinja.pocoo.org/>`_, and :ref:`pyramid_jinja2 Overview + <jinja2:overview>`. + + +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`: + +.. literalinclude:: quick_tour/static_assets/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 + +This tells our WSGI application to map requests under +http://localhost:6543/static/ to files and directories inside a ``static`` +directory alongside our Python module. + +Next make a directory named ``static``, and place ``app.css`` inside: + +.. literalinclude:: quick_tour/static_assets/static/app.css + :language: css + +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 + :language: jinja + :linenos: + :lines: 4-6 + :lineno-start: 4 + :emphasize-lines: 2 + +This link presumes that our CSS is at a URL starting with ``/static/``. What if +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 + :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. + +.. seealso:: See also: + :ref:`Quick Tutorial Static Assets <qtut_static_assets>`, + :doc:`../narr/assets`, :ref:`preventing_http_caching`, and + :ref:`influencing_http_caching`. + + +Returning JSON +============== + +Modern web apps are more than rendered HTML. Dynamic pages now use JavaScript +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 + :linenos: + :lines: 9- + :lineno-start: 9 + +This wires up a view that returns some data through the JSON :term:`renderer`, +which calls Python's JSON support to serialize the data into JSON, and sets the +appropriate HTTP headers. + +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 + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 + +.. seealso:: See also: + :ref:`Quick Tutorial JSON <qtut_json>`, :ref:`views_which_use_a_renderer`, + :ref:`json_renderer`, and :ref:`adding_and_overriding_renderers`. + + +View classes +============ + +So far our views have been simple, free-standing functions. Many times your +views are related. They may have different ways to look at or work on the same +data, or they may be a REST API that handles multiple operations. Grouping +these together as a :ref:`view class <class_as_view>` makes sense and achieves +the following goals. + +- Group views + +- Centralize some repetitive defaults + +- Share some state and helpers + +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 + :linenos: + :lines: 7- + :lineno-start: 7 + +As you can see, the three views are logically grouped together. Specifically: + +- The first view is returned when you go to ``/howdy/amy``. This URL is mapped + to the ``hello`` route that we centrally set using the optional + ``@view_defaults``. + +- The second view is returned when the form data contains a field with + ``form.edit``, such as clicking on ``<input type="submit" name="form.edit" + value="Save">``. This rule is specified in the ``@view_config`` for that + view. + +- The third view is returned when clicking on a button such as ``<input + type="submit" name="form.delete" value="Delete">``. + +Only one route is needed, stated in one place atop the view class. Also, the +assignment of ``name`` is done in the ``__init__`` function. Our templates can +then use ``{{ view.name }}``. + +Pyramid view classes, combined with built-in and custom predicates, have much +more to offer: + +- All the same view configuration parameters as function views + +- One route leading to multiple views, based on information in the request or + data such as ``request_param``, ``request_method``, ``accept``, ``header``, + ``xhr``, ``containment``, and ``custom_predicates`` + +.. seealso:: See also: + :ref:`Quick Tutorial View Classes <qtut_view_classes>`, :ref:`Quick + Tutorial More View Classes <qtut_more_view_classes>`, and + :ref:`class_as_view`. + + +Quick project startup with scaffolds +==================================== + +So far we have done all of our *Quick Tour* as a single Python file. No Python +packages, no structure. Most Pyramid projects, though, aren't developed this +way. + +To ease the process of getting started, Pyramid provides *scaffolds* that +generate sample projects from templates in Pyramid and Pyramid add-ons. +Pyramid's ``pcreate`` command can list the available scaffolds: + +.. code-block:: bash + + $ pcreate --list + Available scaffolds: + alchemy: Pyramid SQLAlchemy project using url dispatch + pyramid_jinja2_starter: Pyramid Jinja2 starter project + starter: Pyramid starter project + zodb: Pyramid ZODB project using traversal + +The ``pyramid_jinja2`` add-on gave us a scaffold that we can use. From the +parent directory of where we want our Python package to be generated, let's use +that scaffold to make our project: + +.. code-block:: bash + + $ pcreate --scaffold pyramid_jinja2_starter hello_world + +We next use the normal Python command to set up our package for development: + +.. code-block:: bash + + $ cd hello_world + $ $VENV/bin/pip install -e . + +We are moving in the direction of a full-featured Pyramid project, with a +proper setup for Python standards (packaging) and Pyramid configuration. This +includes a new way of running your application: + +.. code-block:: bash + + $ $VENV/bin/pserve development.ini + +Let's look at ``pserve`` and configuration in more depth. + +.. seealso:: See also: + :ref:`Quick Tutorial Scaffolds <qtut_scaffolds>`, + :ref:`project_narr`, and + :doc:`../narr/scaffolding` + +Application running with ``pserve`` +=================================== + +Prior to scaffolds, our project mixed a number of operational details into our +code. Why should my main code care which HTTP server I want and what port +number to run on? + +``pserve`` is Pyramid's application runner, separating operational details from +your code. When you install Pyramid, a small command program called ``pserve`` +is written to your ``bin`` directory. This program is an executable Python +module. It's very small, getting most of its brains via import. + +You can run ``pserve`` with ``--help`` to see some of its options. Doing so +reveals that you can ask ``pserve`` to watch your development files and reload +the server when they change: + +.. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +The ``pserve`` command has a number of other options and operations. Most of +the work, though, comes from your project's wiring, as expressed in the +configuration file you supply to ``pserve``. Let's take a look at this +configuration file. + +.. seealso:: See also: + :ref:`what_is_this_pserve_thing` + +Configuration with ``.ini`` files +================================= + +Earlier in *Quick Tour* we first met Pyramid's configuration system. At that +point we did all configuration in Python code. For example, the port number +chosen for our HTTP server was right there in Python code. Our scaffold has +moved this decision and more into the ``development.ini`` file: + +.. literalinclude:: quick_tour/package/development.ini + :language: ini + +Let's take a quick high-level look. First the ``.ini`` file is divided into +sections: + +- ``[app:main]`` configures our WSGI app + +- ``[server:main]`` holds our WSGI server settings + +- Various sections afterwards configure our Python logging system + +We have a few decisions made for us in this configuration: + +#. *Choice of web server:* ``use = egg:hello_world`` tells ``pserve`` to + use the ``waitress`` server. + +#. *Port number:* ``port = 6543`` tells ``waitress`` to listen on port 6543. + +#. *WSGI app:* What package has our WSGI application in it? + ``use = egg:hello_world`` in the app section tells the configuration what + application to load. + +#. *Easier development by automatic template reloading:* In development mode, + you shouldn't have to restart the server when editing a Jinja2 template. + ``pyramid.reload_templates = true`` sets this policy, which might be + different in production. + +Additionally the ``development.ini`` generated by this scaffold wired up +Python's standard logging. We'll now see in the console, for example, a log on +every request that comes in, as well as traceback information. + +.. seealso:: See also: + :ref:`Quick Tutorial Application Configuration <qtut_ini>`, + :ref:`environment_chapter` and + :doc:`../narr/paste` + + +Easier development with ``debugtoolbar`` +======================================== + +As we introduce the basics, we also want to show how to be productive in +development and debugging. For example, we just discussed template reloading +and earlier we showed ``--reload`` for application reloading. + +``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes several tools +available in your browser. Adding it to your project illustrates several points +about configuration. + +The scaffold ``pyramid_jinja2_starter`` is already configured to include the +add-on ``pyramid_debugtoolbar`` in its ``setup.py``: + +.. literalinclude:: quick_tour/package/setup.py + :language: python + :linenos: + :lineno-start: 11 + :lines: 11-16 + +It was installed when you previously ran: + +.. code-block:: bash + + $ $VENV/bin/pip install -e . + +The ``pyramid_debugtoolbar`` package is a Pyramid add-on, which means we need +to include its configuration into our web application. The ``pyramid_jinja2`` +add-on already took care of this for us in its ``__init__.py``: + +.. literalinclude:: quick_tour/package/hello_world/__init__.py + :language: python + :linenos: + :lineno-start: 16 + :lines: 19 + +And it uses the ``pyramid.includes`` facility in our ``development.ini``: + +.. literalinclude:: quick_tour/package/development.ini + :language: ini + :linenos: + :lineno-start: 15 + :lines: 15-16 + +You'll now see a Pyramid logo on the right side of your browser window, which +when clicked opens a new window that provides introspective access to debugging +information. Even better, if your web application generates an error, you will +see a nice traceback on the screen. When you want to disable this toolbar, +there's no need to change code: you can remove it from ``pyramid.includes`` in +the relevant ``.ini`` configuration file. + +.. seealso:: See also: + :ref:`Quick Tutorial pyramid_debugtoolbar <qtut_debugtoolbar>` and + :ref:`pyramid_debugtoolbar <toolbar:overview>` + +Unit tests and ``py.test`` +========================== + +Yikes! We got this far and we haven't yet discussed tests. This is particularly +egregious, as Pyramid has had a deep commitment to full test coverage since +before its release. + +Our ``pyramid_jinja2_starter`` scaffold generated a ``tests.py`` module with +one unit test in it. To run it, let's install the handy ``pytest`` test runner +by editing ``setup.py``. While we're at it, we'll throw in the ``pytest-cov`` +tool which yells at us for code that isn't tested. Insert and edit the +following lines as shown: + +.. code-block:: python + :linenos: + :lineno-start: 11 + :emphasize-lines: 8-12 + + requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + ] + + tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +.. code-block:: python + :linenos: + :lineno-start: 34 + :emphasize-lines: 2-4 + + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + +We changed ``setup.py`` which means we need to rerun ``$VENV/bin/pip install -e +".[testing]"``. We can now run all our tests: + +.. code-block:: bash + + $ $VENV/bin/py.test --cov=hello_world --cov-report=term-missing hello_world/tests.py + +This yields the following output. + +.. code-block:: text + + =========================== test session starts =========================== + platform darwin -- Python 3.5.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + rootdir: /Users/stevepiercy/projects/hack-on-pyramid/hello_world, inifile: + plugins: cov-2.2.1 + collected 1 items + + hello_world/tests.py . + ------------- coverage: platform darwin, python 3.5.0-final-0 ------------- + Name Stmts Miss Cover Missing + -------------------------------------------------------- + hello_world/__init__.py 11 8 27% 11-23 + hello_world/resources.py 5 1 80% 8 + hello_world/tests.py 14 0 100% + hello_world/views.py 4 0 100% + -------------------------------------------------------- + TOTAL 34 9 74% + + ========================= 1 passed in 0.22 seconds ========================= + +Our unit test passed, although its coverage is incomplete. What did our test +look like? + +.. literalinclude:: quick_tour/package/hello_world/tests.py + :linenos: + +Pyramid supplies helpers for test writing, which we use in the test setup and +teardown. Our one test imports the view, makes a dummy request, and sees if the +view returns what we expected. + +.. seealso:: See also: + :ref:`Quick Tutorial Unit Testing <qtut_unit_testing>`, :ref:`Quick + Tutorial Functional Testing <qtut_functional_testing>`, and + :ref:`testing_chapter` + +Logging +======= + +It's important to know what is going on inside our web application. In +development we might need to collect some output. In production we might need +to detect situations when other people use the site. We need *logging*. + +Fortunately Pyramid uses the normal Python approach to logging. The scaffold +generated in your ``development.ini`` has a number of lines that configure the +logging for you to some reasonable defaults. You then see messages sent by +Pyramid (for example, when a new request comes in). + +Maybe you would like to log messages in your code? In your Python module, +import and set up the logging: + +.. literalinclude:: quick_tour/package/hello_world/views.py + :language: python + :linenos: + :lineno-start: 3 + :lines: 3-4 + +You can now, in your code, log messages: + +.. literalinclude:: quick_tour/package/hello_world/views.py + :language: python + :linenos: + :lineno-start: 9 + :lines: 9-10 + :emphasize-lines: 2 + +This will log ``Some Message`` at a ``debug`` log level to the +application-configured logger in your ``development.ini``. What controls that? +These emphasized sections in the configuration file: + +.. literalinclude:: quick_tour/package/development.ini + :language: ini + :linenos: + :lineno-start: 36 + :lines: 36-52 + :emphasize-lines: 1-2,14-17 + +Our application, a package named ``hello_world``, is set up as a logger and +configured to log messages at a ``DEBUG`` or higher level. When you visit +http://localhost:6543, your console will now show: + +.. code-block:: text + + 2016-01-18 13:55:55,040 DEBUG [hello_world.views:10][waitress] Some Message + +.. seealso:: See also: + :ref:`Quick Tutorial Logging <qtut_logging>` and :ref:`logging_chapter`. + +Sessions +======== + +When people use your web application, they frequently perform a task that +requires semi-permanent data to be saved. For example, a shopping cart. This is +called a :term:`session`. + +Pyramid has basic built-in support for sessions. Third party packages such as +``pyramid_redis_sessions`` provide richer session support. Or you can create +your own custom sessioning engine. Let's take a look at the :doc:`built-in +sessioning support <../narr/sessions>`. In our ``__init__.py`` we first import +the kind of sessioning we want: + +.. literalinclude:: quick_tour/package/hello_world/__init__.py + :language: python + :linenos: + :lineno-start: 2 + :lines: 2-3 + :emphasize-lines: 2 + +.. warning:: + + As noted in the session docs, this example implementation is not intended + for use in settings with security implications. + +Now make a "factory" and pass it to the :term:`configurator`'s +``session_factory`` argument: + +.. literalinclude:: quick_tour/package/hello_world/__init__.py + :language: python + :linenos: + :lineno-start: 13 + :lines: 13-17 + :emphasize-lines: 3-5 + +Pyramid's :term:`request` object now has a ``session`` attribute that we can +use in our view code in ``views.py``: + +.. literalinclude:: quick_tour/package/hello_world/views.py + :language: python + :linenos: + :lineno-start: 9 + :lines: 9-15 + :emphasize-lines: 3-7 + +We need to update our Jinja2 template to show counter increment in the session: + +.. literalinclude:: quick_tour/package/hello_world/templates/mytemplate.jinja2 + :language: jinja + :linenos: + :lineno-start: 40 + :lines: 40-42 + :emphasize-lines: 3 + +.. seealso:: See also: + :ref:`Quick Tutorial Sessions <qtut_sessions>`, :ref:`sessions_chapter`, + :ref:`flash_messages`, :ref:`session_module`, and + :term:`pyramid_redis_sessions`. + + +Databases +========= + +Web applications mean data. Data means databases. Frequently SQL databases. SQL +databases frequently mean an "ORM" (object-relational mapper.) In Python, ORM +usually leads to the mega-quality *SQLAlchemy*, a Python package that greatly +eases working with databases. + +Pyramid and SQLAlchemy are great friends. That friendship includes a scaffold! + +.. code-block:: bash + + $ $VENV/bin/pcreate --scaffold alchemy sqla_demo + $ cd sqla_demo + $ $VENV/bin/pip install -e . + +We now have a working sample SQLAlchemy application with all dependencies +installed. The sample project provides a console script to initialize a SQLite +database with tables. Let's run it, then start the application: + +.. code-block:: bash + + $ $VENV/bin/initialize_sqla_demo_db development.ini + $ $VENV/bin/pserve development.ini + +The ORM eases the mapping of database structures into a programming language. +SQLAlchemy uses "models" for this mapping. The scaffold generated a sample +model: + +.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models/mymodel.py + :start-after: Start Sphinx Include + :end-before: End Sphinx Include + +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 + :start-after: Start Sphinx Include + :end-before: End Sphinx Include + +.. seealso:: See also: + :ref:`Quick Tutorial Databases <qtut_databases>`, `SQLAlchemy + <http://www.sqlalchemy.org/>`_, :ref:`making_a_console_script`, + :ref:`bfg_sql_wiki_tutorial`, and :ref:`Application Transactions with + pyramid_tm <tm:overview>`. + + +Forms +===== + +Developers have lots of opinions about web forms, thus there are many form +libraries for Python. Pyramid doesn't directly bundle a form library, but +*Deform* is a popular choice for forms, along with its related *Colander* +schema system. + +As an example, imagine we want a form that edits a wiki page. The form should +have two fields on it, one of them a required title and the other a rich text +editor for the body. With Deform we can express this as a Colander schema: + +.. code-block:: python + + class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + +With this in place, we can render the HTML for a form, perhaps with form data +from an existing page: + +.. code-block:: python + + form = self.wiki_form.render() + +We'd like to handle form submission, validation, and saving: + +.. code-block:: python + + # Get the form data that was posted + controls = self.request.POST.items() + try: + # Validate and either raise a validation error + # or return deserialized data from widgets + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Bail out and render form with errors + return dict(title=title, page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + +Deform and Colander provide a very flexible combination for forms, widgets, +schemas, and validation. Recent versions of Deform also include a :ref:`retail +mode <deform:retail>` for gaining Deform features on custom forms. + +Also the ``deform_bootstrap`` Pyramid add-on restyles the stock Deform widgets +using attractive CSS from Twitter Bootstrap and more powerful widgets from +Chosen. + +.. seealso:: See also: + :ref:`Quick Tutorial Forms <qtut_forms>`, :ref:`Deform <deform:overview>`, + :ref:`Colander <colander:overview>`, and `deform_bootstrap + <https://pypi.python.org/pypi/deform_bootstrap>`_. + +Conclusion +========== + +This *Quick Tour* covered a little about a lot. We introduced a long list +of concepts in Pyramid, many of which are expanded on more fully in the +Pyramid developer docs. diff --git a/docs/quick_tour/hello_world/app.py b/docs/quick_tour/hello_world/app.py new file mode 100644 index 000000000..75d22ac96 --- /dev/null +++ b/docs/quick_tour/hello_world/app.py @@ -0,0 +1,16 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<h1>Hello World!</h1>') + + +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() diff --git a/docs/quick_tour/jinja2/app.py b/docs/quick_tour/jinja2/app.py new file mode 100644 index 000000000..b7632807b --- /dev/null +++ b/docs/quick_tour/jinja2/app.py @@ -0,0 +1,11 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{name}') + config.include('pyramid_jinja2') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/jinja2/hello_world.jinja2 b/docs/quick_tour/jinja2/hello_world.jinja2 new file mode 100644 index 000000000..7a902dd3a --- /dev/null +++ b/docs/quick_tour/jinja2/hello_world.jinja2 @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Hello World</title> +</head> +<body> +<h1>Hello {{ name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/jinja2/views.py b/docs/quick_tour/jinja2/views.py new file mode 100644 index 000000000..7dbb45287 --- /dev/null +++ b/docs/quick_tour/jinja2/views.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='hello', renderer='hello_world.jinja2') +def hello_world(request): + return dict(name=request.matchdict['name']) diff --git a/docs/quick_tour/json/app.py b/docs/quick_tour/json/app.py new file mode 100644 index 000000000..40faddd00 --- /dev/null +++ b/docs/quick_tour/json/app.py @@ -0,0 +1,13 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{name}') + config.add_route('hello_json', 'hello.json') + config.add_static_view(name='static', path='static') + config.include('pyramid_jinja2') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/json/hello_world.jinja2 b/docs/quick_tour/json/hello_world.jinja2 new file mode 100644 index 000000000..4fb9be074 --- /dev/null +++ b/docs/quick_tour/json/hello_world.jinja2 @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Hello World</title> + <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/> +</head> +<body> +<h1>Hello {{ name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/json/views.py b/docs/quick_tour/json/views.py new file mode 100644 index 000000000..22aa8aad6 --- /dev/null +++ b/docs/quick_tour/json/views.py @@ -0,0 +1,11 @@ +from pyramid.view import view_config + + +@view_config(route_name='hello', renderer='hello_world.jinja2') +def hello_world(request): + return dict(name=request.matchdict['name']) + + +@view_config(route_name='hello_json', renderer='json') +def hello_json(request): + return [1, 2, 3] diff --git a/docs/quick_tour/package/CHANGES.txt b/docs/quick_tour/package/CHANGES.txt new file mode 100644 index 000000000..ffa255da8 --- /dev/null +++ b/docs/quick_tour/package/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/quick_tour/package/MANIFEST.in b/docs/quick_tour/package/MANIFEST.in new file mode 100644 index 000000000..1d0352f7d --- /dev/null +++ b/docs/quick_tour/package/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include hello_world *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.jinja2 *.js *.html *.xml diff --git a/docs/quick_tour/package/README.txt b/docs/quick_tour/package/README.txt new file mode 100644 index 000000000..63aaf6fbd --- /dev/null +++ b/docs/quick_tour/package/README.txt @@ -0,0 +1,4 @@ +hello_world README + + + diff --git a/docs/quick_tour/package/development.ini b/docs/quick_tour/package/development.ini new file mode 100644 index 000000000..20f9817a9 --- /dev/null +++ b/docs/quick_tour/package/development.ini @@ -0,0 +1,61 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/narr/environment.html +### + +[app:main] +use = egg:hello_world + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = true +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/narr/logging.html +### + +[loggers] +keys = root, hello_world + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_hello_world] +level = DEBUG +handlers = +qualname = hello_world + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/package/hello_world/__init__.py b/docs/quick_tour/package/hello_world/__init__.py new file mode 100644 index 000000000..97f93d5a8 --- /dev/null +++ b/docs/quick_tour/package/hello_world/__init__.py @@ -0,0 +1,26 @@ +from pyramid.config import Configurator +from hello_world.resources import get_root +from pyramid.session import SignedCookieSessionFactory + + +def main(global_config, **settings): + """ This function returns a WSGI application. + + It is usually called by the PasteDeploy framework during + ``paster serve``. + """ + settings = dict(settings) + settings.setdefault('jinja2.i18n.domain', 'hello_world') + + my_session_factory = SignedCookieSessionFactory('itsaseekreet') + config = Configurator(root_factory=get_root, settings=settings, + session_factory=my_session_factory) + config.add_translation_dirs('locale/') + config.include('pyramid_jinja2') + + config.add_static_view('static', 'static') + config.add_view('hello_world.views.my_view', + context='hello_world.resources.MyResource', + renderer="templates/mytemplate.jinja2") + + return config.make_wsgi_app() diff --git a/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.mo b/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.mo Binary files differnew file mode 100644 index 000000000..40bf0c271 --- /dev/null +++ b/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.mo diff --git a/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.po b/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.po new file mode 100644 index 000000000..0df243dba --- /dev/null +++ b/docs/quick_tour/package/hello_world/locale/de/LC_MESSAGES/hello_world.po @@ -0,0 +1,21 @@ +# Translations template for PROJECT. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-05-12 09:14-0330\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +msgid "Hello!" +msgstr "Hallo!" diff --git a/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.mo b/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.mo Binary files differnew file mode 100644 index 000000000..4fc438bfe --- /dev/null +++ b/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.mo diff --git a/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.po b/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.po new file mode 100644 index 000000000..dc0aae5d7 --- /dev/null +++ b/docs/quick_tour/package/hello_world/locale/fr/LC_MESSAGES/hello_world.po @@ -0,0 +1,21 @@ +# Translations template for PROJECT. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-05-12 09:14-0330\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +msgid "Hello!" +msgstr "Bonjour!" diff --git a/docs/quick_tour/package/hello_world/locale/hello_world.pot b/docs/quick_tour/package/hello_world/locale/hello_world.pot new file mode 100644 index 000000000..9c9460cb2 --- /dev/null +++ b/docs/quick_tour/package/hello_world/locale/hello_world.pot @@ -0,0 +1,21 @@ +# Translations template for PROJECT. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-05-12 09:14-0330\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +msgid "Hello!" +msgstr "" diff --git a/docs/quick_tour/package/hello_world/resources.py b/docs/quick_tour/package/hello_world/resources.py new file mode 100644 index 000000000..e89c2f363 --- /dev/null +++ b/docs/quick_tour/package/hello_world/resources.py @@ -0,0 +1,8 @@ +class MyResource(object): + pass + +root = MyResource() + + +def get_root(request): + return root diff --git a/docs/narr/MyProject/myproject/static/favicon.ico b/docs/quick_tour/package/hello_world/static/favicon.ico Binary files differindex 71f837c9e..71f837c9e 100644 --- a/docs/narr/MyProject/myproject/static/favicon.ico +++ b/docs/quick_tour/package/hello_world/static/favicon.ico diff --git a/docs/quick_tour/package/hello_world/static/pyramid-16x16.png b/docs/quick_tour/package/hello_world/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/quick_tour/package/hello_world/static/pyramid-16x16.png diff --git a/docs/quick_tour/package/hello_world/static/pyramid.png b/docs/quick_tour/package/hello_world/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/quick_tour/package/hello_world/static/pyramid.png diff --git a/docs/quick_tour/package/hello_world/static/theme.css b/docs/quick_tour/package/hello_world/static/theme.css new file mode 100644 index 000000000..e3cf3f290 --- /dev/null +++ b/docs/quick_tour/package/hello_world/static/theme.css @@ -0,0 +1,153 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} + diff --git a/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 b/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 new file mode 100644 index 000000000..a6089aebc --- /dev/null +++ b/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('hello_world:static/pyramid-16x16.png')}}"> + + <title>Starter Scaffold for Pyramid Jinja2</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('hello_world:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('hello_world:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1> + <span class="font-semi-bold">Pyramid</span> + <span class="smaller">Jinja2 scaffold</span> + </h1> + <p class="lead"> + {% trans %}Hello{% endtrans %} to <span class="font-normal">{{project}}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.6</span>.</p> + <p>Counter: {{ request.session.counter }}</p> + </div> + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.6</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/quick_tour/package/hello_world/tests.py b/docs/quick_tour/package/hello_world/tests.py new file mode 100644 index 000000000..ccec14f70 --- /dev/null +++ b/docs/quick_tour/package/hello_world/tests.py @@ -0,0 +1,20 @@ +import unittest +from pyramid import testing +from pyramid.i18n import TranslationStringFactory + +_ = TranslationStringFactory('hello_world') + + +class ViewTests(unittest.TestCase): + + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from hello_world.views import my_view + request = testing.DummyRequest() + response = my_view(request) + self.assertEqual(response['project'], 'hello_world') diff --git a/docs/quick_tour/package/hello_world/views.py b/docs/quick_tour/package/hello_world/views.py new file mode 100644 index 000000000..9f7953c8e --- /dev/null +++ b/docs/quick_tour/package/hello_world/views.py @@ -0,0 +1,16 @@ +from pyramid.i18n import TranslationStringFactory + +import logging +log = logging.getLogger(__name__) + +_ = TranslationStringFactory('hello_world') + + +def my_view(request): + log.debug('Some Message') + session = request.session + if 'counter' in session: + session['counter'] += 1 + else: + session['counter'] = 0 + return {'project': 'hello_world'} diff --git a/docs/quick_tour/package/message-extraction.ini b/docs/quick_tour/package/message-extraction.ini new file mode 100644 index 000000000..0c3d54bc1 --- /dev/null +++ b/docs/quick_tour/package/message-extraction.ini @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **.jinja2] +encoding = utf-8 diff --git a/docs/quick_tour/package/setup.cfg b/docs/quick_tour/package/setup.cfg new file mode 100644 index 000000000..186e796fc --- /dev/null +++ b/docs/quick_tour/package/setup.cfg @@ -0,0 +1,28 @@ +[nosetests] +match = ^test +nocapture = 1 +cover-package = hello_world +with-coverage = 1 +cover-erase = 1 + +[compile_catalog] +directory = hello_world/locale +domain = hello_world +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = hello_world/locale/hello_world.pot +width = 80 +mapping_file = message-extraction.ini + +[init_catalog] +domain = hello_world +input_file = hello_world/locale/hello_world.pot +output_dir = hello_world/locale + +[update_catalog] +domain = hello_world +input_file = hello_world/locale/hello_world.pot +output_dir = hello_world/locale +previous = true diff --git a/docs/quick_tour/package/setup.py b/docs/quick_tour/package/setup.py new file mode 100644 index 000000000..61ed3c406 --- /dev/null +++ b/docs/quick_tour/package/setup.py @@ -0,0 +1,44 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', +] + +setup(name='hello_world', + version='0.0', + description='hello_world', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require={ + 'testing': ['nose', 'coverage'], + }, + test_suite="hello_world", + entry_points="""\ + [paste.app_factory] + main = hello_world:main + """, + ) diff --git a/docs/quick_tour/requests/app.py b/docs/quick_tour/requests/app.py new file mode 100644 index 000000000..f55264cff --- /dev/null +++ b/docs/quick_tour/requests/app.py @@ -0,0 +1,24 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + # Some parameters from a request such as /?name=lisa + url = request.url + name = request.params.get('name', 'No Name Provided') + + body = 'URL %s with name: %s' % (url, name) + return Response( + content_type="text/plain", + body=body + ) + + +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() diff --git a/docs/quick_tour/routing/app.py b/docs/quick_tour/routing/app.py new file mode 100644 index 000000000..12b547bfe --- /dev/null +++ b/docs/quick_tour/routing/app.py @@ -0,0 +1,10 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{first}/{last}') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever()
\ No newline at end of file diff --git a/docs/quick_tour/routing/views.py b/docs/quick_tour/routing/views.py new file mode 100644 index 000000000..8a3bd230e --- /dev/null +++ b/docs/quick_tour/routing/views.py @@ -0,0 +1,8 @@ +from pyramid.response import Response +from pyramid.view import view_config + + +@view_config(route_name='hello') +def hello_world(request): + body = '<h1>Hi %(first)s %(last)s!</h1>' % request.matchdict + return Response(body) diff --git a/docs/quick_tour/sqla_demo/CHANGES.txt b/docs/quick_tour/sqla_demo/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/quick_tour/sqla_demo/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/quick_tour/sqla_demo/MANIFEST.in b/docs/quick_tour/sqla_demo/MANIFEST.in new file mode 100644 index 000000000..a432577e9 --- /dev/null +++ b/docs/quick_tour/sqla_demo/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include sqla_demo *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/quick_tour/sqla_demo/README.txt b/docs/quick_tour/sqla_demo/README.txt new file mode 100644 index 000000000..b6d4c7798 --- /dev/null +++ b/docs/quick_tour/sqla_demo/README.txt @@ -0,0 +1,14 @@ +sqla_demo README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_sqla_demo_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/docs/quick_tour/sqla_demo/development.ini b/docs/quick_tour/sqla_demo/development.ini new file mode 100644 index 000000000..0db0950a0 --- /dev/null +++ b/docs/quick_tour/sqla_demo/development.ini @@ -0,0 +1,71 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:sqla_demo + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/sqla_demo.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, sqla_demo, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_sqla_demo] +level = DEBUG +handlers = +qualname = sqla_demo + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/production.ini b/docs/quick_tour/sqla_demo/production.ini new file mode 100644 index 000000000..38f3b6318 --- /dev/null +++ b/docs/quick_tour/sqla_demo/production.ini @@ -0,0 +1,62 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:sqla_demo + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/sqla_demo.sqlite + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, sqla_demo, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqla_demo] +level = WARN +handlers = +qualname = sqla_demo + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/setup.cfg b/docs/quick_tour/sqla_demo/setup.cfg new file mode 100644 index 000000000..9f91cd122 --- /dev/null +++ b/docs/quick_tour/sqla_demo/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match=^test +nocapture=1 +cover-package=sqla_demo +with-coverage=1 +cover-erase=1 + +[compile_catalog] +directory = sqla_demo/locale +domain = sqla_demo +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = sqla_demo/locale/sqla_demo.pot +width = 80 + +[init_catalog] +domain = sqla_demo +input_file = sqla_demo/locale/sqla_demo.pot +output_dir = sqla_demo/locale + +[update_catalog] +domain = sqla_demo +input_file = sqla_demo/locale/sqla_demo.pot +output_dir = sqla_demo/locale +previous = true diff --git a/docs/quick_tour/sqla_demo/setup.py b/docs/quick_tour/sqla_demo/setup.py new file mode 100644 index 000000000..312a97c06 --- /dev/null +++ b/docs/quick_tour/sqla_demo/setup.py @@ -0,0 +1,47 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +setup(name='sqla_demo', + version='0.0', + description='sqla_demo', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='sqla_demo', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = sqla_demo:main + [console_scripts] + initialize_sqla_demo_db = sqla_demo.scripts.initializedb:main + """, + ) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py new file mode 100644 index 000000000..7994bbfa8 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py @@ -0,0 +1,13 @@ +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.meta') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() + return config.make_wsgi_app() diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py new file mode 100644 index 000000000..6ffc10a78 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py @@ -0,0 +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 + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py new file mode 100644 index 000000000..80ececd8c --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py @@ -0,0 +1,49 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.schema import MetaData +import zope.sqlalchemy + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py new file mode 100644 index 000000000..eb645bfe6 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/mymodel.py @@ -0,0 +1,19 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + + +# Start Sphinx Include +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + # End Sphinx Include + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/scripts/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py new file mode 100644 index 000000000..f0d09729e --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py @@ -0,0 +1,45 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import ( + Base, + get_session, + get_engine, + get_dbmaker, + ) +from ..models.mymodel import MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + + Base.metadata.create_all(engine) + + with transaction.manager: + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 new file mode 100644 index 000000000..76a098122 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/layout.jinja2 @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('sqla_demo:static/pyramid-16x16.png')}}"> + + <title>Alchemy Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('sqla_demo:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('sqla_demo:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + {% block content %} + <p>No content</p> + {% endblock content %} + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7.dev0</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7.dev0</span>.</p> +</div> +{% endblock content %} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/tests.py b/docs/quick_tour/sqla_demo/sqla_demo/tests.py new file mode 100644 index 000000000..b6b6fdf4d --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() + + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'sqla_demo') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/views/__init__.py diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views/default.py b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py new file mode 100644 index 000000000..e5e70cf9d --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/views/default.py @@ -0,0 +1,35 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models.mymodel import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + # Start Sphinx Include + one = query.filter(MyModel.name == 'one').first() + # End Sphinx Include + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status_int=500) + return {'one': one, 'project': 'sqla_demo'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_sqla_demo_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/docs/quick_tour/static_assets/app.py b/docs/quick_tour/static_assets/app.py new file mode 100644 index 000000000..1849c0a5a --- /dev/null +++ b/docs/quick_tour/static_assets/app.py @@ -0,0 +1,12 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{name}') + config.add_static_view(name='static', path='static') + config.include('pyramid_jinja2') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/static_assets/hello_world.jinja2 b/docs/quick_tour/static_assets/hello_world.jinja2 new file mode 100644 index 000000000..0fb2ce296 --- /dev/null +++ b/docs/quick_tour/static_assets/hello_world.jinja2 @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Hello World</title> + <link rel="stylesheet" href="/static/app.css"/> +</head> +<body> +<h1>Hello {{ name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/static_assets/hello_world_static.jinja2 b/docs/quick_tour/static_assets/hello_world_static.jinja2 new file mode 100644 index 000000000..4fb9be074 --- /dev/null +++ b/docs/quick_tour/static_assets/hello_world_static.jinja2 @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Hello World</title> + <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/> +</head> +<body> +<h1>Hello {{ name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/static_assets/static/app.css b/docs/quick_tour/static_assets/static/app.css new file mode 100644 index 000000000..f8acf3164 --- /dev/null +++ b/docs/quick_tour/static_assets/static/app.css @@ -0,0 +1,4 @@ +body { + margin: 2em; + font-family: sans-serif; +}
\ No newline at end of file diff --git a/docs/quick_tour/static_assets/views.py b/docs/quick_tour/static_assets/views.py new file mode 100644 index 000000000..7dbb45287 --- /dev/null +++ b/docs/quick_tour/static_assets/views.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='hello', renderer='hello_world.jinja2') +def hello_world(request): + return dict(name=request.matchdict['name']) diff --git a/docs/quick_tour/templating/app.py b/docs/quick_tour/templating/app.py new file mode 100644 index 000000000..52b7faf55 --- /dev/null +++ b/docs/quick_tour/templating/app.py @@ -0,0 +1,11 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{name}') + config.include('pyramid_chameleon') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/templating/hello_world.pt b/docs/quick_tour/templating/hello_world.pt new file mode 100644 index 000000000..ae14f447d --- /dev/null +++ b/docs/quick_tour/templating/hello_world.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Glance</title> +</head> +<body> +<h1>Hello ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/templating/views.py b/docs/quick_tour/templating/views.py new file mode 100644 index 000000000..90730ae32 --- /dev/null +++ b/docs/quick_tour/templating/views.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='hello', renderer='hello_world.pt') +def hello_world(request): + return dict(name=request.matchdict['name']) diff --git a/docs/quick_tour/view_classes/app.py b/docs/quick_tour/view_classes/app.py new file mode 100644 index 000000000..40faddd00 --- /dev/null +++ b/docs/quick_tour/view_classes/app.py @@ -0,0 +1,13 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/howdy/{name}') + config.add_route('hello_json', 'hello.json') + config.add_static_view(name='static', path='static') + config.include('pyramid_jinja2') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/view_classes/delete.jinja2 b/docs/quick_tour/view_classes/delete.jinja2 new file mode 100644 index 000000000..ba45b7d16 --- /dev/null +++ b/docs/quick_tour/view_classes/delete.jinja2 @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Delete World</title> +</head> +<body> +<h1>Delete {{ view.name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/view_classes/edit.jinja2 b/docs/quick_tour/view_classes/edit.jinja2 new file mode 100644 index 000000000..ce0eb5bd1 --- /dev/null +++ b/docs/quick_tour/view_classes/edit.jinja2 @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Edit World</title> +</head> +<body> +<h1>Edit {{ view.name }}!</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/view_classes/hello.jinja2 b/docs/quick_tour/view_classes/hello.jinja2 new file mode 100644 index 000000000..fc3058067 --- /dev/null +++ b/docs/quick_tour/view_classes/hello.jinja2 @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Hello World</title> +</head> +<body> +<h1>Hello {{ view.name }}!</h1> +<form method="POST" + action="{{ request.current_route_url() }}"> + <input name="new_name"> + <input type="submit" name="form.edit" value="Save"> + <input type="submit" name="form.delete" value="Delete"> +</form> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tour/view_classes/views.py b/docs/quick_tour/view_classes/views.py new file mode 100644 index 000000000..10ff238c7 --- /dev/null +++ b/docs/quick_tour/view_classes/views.py @@ -0,0 +1,30 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +# One route, at /howdy/amy, so don't repeat on each @view_config +@view_defaults(route_name='hello') +class HelloWorldViews: + def __init__(self, request): + self.request = request + # Our templates can now say {{ view.name }} + self.name = request.matchdict['name'] + + # Retrieving /howdy/amy the first time + @view_config(renderer='hello.jinja2') + def hello_view(self): + return dict() + + # Posting to /howdy/amy via the "Edit" submit button + @view_config(request_param='form.edit', renderer='edit.jinja2') + def edit_view(self): + print('Edited') + return dict() + + # Posting to /howdy/amy via the "Delete" submit button + @view_config(request_param='form.delete', renderer='delete.jinja2') + def delete_view(self): + print('Deleted') + return dict() diff --git a/docs/quick_tour/views/app.py b/docs/quick_tour/views/app.py new file mode 100644 index 000000000..e8df6eff2 --- /dev/null +++ b/docs/quick_tour/views/app.py @@ -0,0 +1,13 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator + +if __name__ == '__main__': + config = Configurator() + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('redirect', '/goto') + config.add_route('exception', '/problem') + config.scan('views') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() diff --git a/docs/quick_tour/views/views.py b/docs/quick_tour/views/views.py new file mode 100644 index 000000000..1449cbb38 --- /dev/null +++ b/docs/quick_tour/views/views.py @@ -0,0 +1,32 @@ +import cgi + +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.view import view_config + + +# First view, available at http://localhost:6543/ +@view_config(route_name='home') +def home_view(request): + return Response('<p>Visit <a href="/howdy?name=lisa">hello</a></p>') + + +# /howdy?name=alice which links to the next view +@view_config(route_name='hello') +def hello_view(request): + name = request.params.get('name', 'No Name') + body = '<p>Hi %s, this <a href="/goto">redirects</a></p>' + # cgi.escape to prevent Cross-Site Scripting (XSS) [CWE 79] + return Response(body % cgi.escape(name)) + + +# /goto which issues HTTP redirect to the last view +@view_config(route_name='redirect') +def redirect_view(request): + return HTTPFound(location="/problem") + + +# /problem which causes a site error +@view_config(route_name='exception') +def exception_view(request): + raise Exception() diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst new file mode 100644 index 000000000..acff97f3b --- /dev/null +++ b/docs/quick_tutorial/authentication.rst @@ -0,0 +1,128 @@ +.. _qtut_authentication: + +============================== +20: Logins With Authentication +============================== + +Login views that authenticate a username and password against a list of users. + + +Background +========== + +Most web applications have URLs that allow people to add/edit/delete content +via a web browser. Time to add :ref:`security <security_chapter>` to the +application. In this first step we introduce authentication. That is, logging +in and logging out, using Pyramid's rich facilities for pluggable user storage. + +In the next step we will introduce protection of resources with authorization +security statements. + + +Objectives +========== + +- Introduce the Pyramid concepts of authentication. + +- Create login and logout views. + +Steps +===== + +#. We are going to use the view classes step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r view_classes authentication; cd authentication + $ $VENV/bin/pip install -e . + +#. Put the security hash in the ``authentication/development.ini`` + configuration file as ``tutorial.secret`` instead of putting it in the code: + + .. literalinclude:: authentication/development.ini + :language: ini + :linenos: + +#. Get authentication (and for now, authorization policies) and login route + into the :term:`configurator` in ``authentication/tutorial/__init__.py``: + + .. literalinclude:: authentication/tutorial/__init__.py + :linenos: + +#. Create an ``authentication/tutorial/security.py`` module that can find our + user information by providing an *authentication policy callback*: + + .. literalinclude:: authentication/tutorial/security.py + :linenos: + +#. Update the views in ``authentication/tutorial/views.py``: + + .. literalinclude:: authentication/tutorial/views.py + :linenos: + +#. Add a login template at ``authentication/tutorial/login.pt``: + + .. literalinclude:: authentication/tutorial/login.pt + :language: html + :linenos: + +#. Provide a login/logout box in ``authentication/tutorial/home.pt``: + + .. literalinclude:: authentication/tutorial/home.pt + :language: html + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + +#. Click the "Log In" link. + +#. Submit the login form with the username ``editor`` and the password + ``editor``. + +#. Note that the "Log In" link has changed to "Logout". + +#. Click the "Logout" link. + +Analysis +======== + +Unlike many web frameworks, Pyramid includes a built-in but optional security +model for authentication and authorization. This security system is intended to +be flexible and support many needs. In this security model, authentication (who +are you) and authorization (what are you allowed to do) are not just pluggable, +but de-coupled. To learn one step at a time, we provide a system that +identifies users and lets them log out. + +In this example we chose to use the bundled :ref:`AuthTktAuthenticationPolicy +<authentication_module>` policy. We enabled it in our configuration and +provided a ticket-signing secret in our INI file. + +Our view class grew a login view. When you reached it via a ``GET`` request, it +returned a login form. When reached via ``POST``, it processed the submitted +username and password against the "groupfinder" callable that we registered in +the configuration. + +In our template, we fetched the ``logged_in`` value from the view class. We use +this to calculate the logged-in user, if any. In the template we can then +choose to show a login link to anonymous visitors or a logout link to logged-in +users. + + +Extra credit +============ + +#. What is the difference between a user and a principal? + +#. Can I use a database behind my ``groupfinder`` to look up principals? + +#. Once I am logged in, does any user-centric information get jammed onto each + request? Use ``import pdb; pdb.set_trace()`` to answer this. + +.. seealso:: See also :ref:`security_chapter`, + :ref:`AuthTktAuthenticationPolicy <authentication_module>`. diff --git a/docs/quick_tutorial/authentication/development.ini b/docs/quick_tutorial/authentication/development.ini new file mode 100644 index 000000000..8a39b2fe7 --- /dev/null +++ b/docs/quick_tutorial/authentication/development.ini @@ -0,0 +1,11 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/authentication/setup.py b/docs/quick_tutorial/authentication/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/authentication/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/__init__.py b/docs/quick_tutorial/authentication/tutorial/__init__.py new file mode 100644 index 000000000..efc09e760 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/__init__.py @@ -0,0 +1,25 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/home.pt b/docs/quick_tutorial/authentication/tutorial/home.pt new file mode 100644 index 000000000..ed911b673 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/home.pt @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> + +<div> + <a tal:condition="view.logged_in is None" + href="${request.application_url}/login">Log In</a> + <a tal:condition="view.logged_in is not None" + href="${request.application_url}/logout">Logout</a> +</div> + +<h1>Hi ${name}</h1> +<p>Visit <a href="${request.route_url('hello')}">hello</a></p> +</body> +</html> diff --git a/docs/quick_tutorial/authentication/tutorial/login.pt b/docs/quick_tutorial/authentication/tutorial/login.pt new file mode 100644 index 000000000..9e5bfe2ad --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/login.pt @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Login</h1> +<span tal:replace="message"/> + +<form action="${url}" method="post"> + <input type="hidden" name="came_from" + value="${came_from}"/> + <label for="login">Username</label> + <input type="text" id="login" + name="login" + value="${login}"/><br/> + <label for="password">Password</label> + <input type="password" id="password" + name="password" + value="${password}"/><br/> + <input type="submit" name="form.submitted" + value="Log In"/> +</form> +</body> +</html> diff --git a/docs/quick_tutorial/authentication/tutorial/security.py b/docs/quick_tutorial/authentication/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, [])
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py new file mode 100644 index 000000000..ab46eb2dd --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -0,0 +1,64 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + +from pyramid.view import ( + view_config, + view_defaults + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = request.authenticated_userid + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/authorization.rst b/docs/quick_tutorial/authorization.rst new file mode 100644 index 000000000..58c1d2582 --- /dev/null +++ b/docs/quick_tutorial/authorization.rst @@ -0,0 +1,117 @@ +.. _qtut_authorization: + +=========================================== +21: Protecting Resources With Authorization +=========================================== + +Assign security statements to resources describing the permissions required to +perform an operation. + + +Background +========== + +Our application has URLs that allow people to add/edit/delete content via a web +browser. Time to add security to the application. Let's protect our add/edit +views to require a login (username of ``editor`` and password of ``editor``). +We will allow the other views to continue working without a password. + + +Objectives +========== + +- Introduce the Pyramid concepts of authentication, authorization, permissions, + and access control lists (ACLs). + +- Make a :term:`root factory` that returns an instance of our class for the top + of the application. + +- Assign security statements to our root resource. + +- Add a permissions predicate on a view. + +- Provide a :term:`Forbidden view` to handle visiting a URL without adequate + permissions. + + +Steps +===== + +#. We are going to use the authentication step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r authentication authorization; cd authorization + $ $VENV/bin/pip install -e . + +#. Start by changing ``authorization/tutorial/__init__.py`` to specify a root + factory to the :term:`configurator`: + + .. literalinclude:: authorization/tutorial/__init__.py + :linenos: + +#. That means we need to implement ``authorization/tutorial/resources.py``: + + .. literalinclude:: authorization/tutorial/resources.py + :linenos: + +#. Change ``authorization/tutorial/views.py`` to require the ``edit`` + permission on the ``hello`` view and implement the forbidden view: + + .. literalinclude:: authorization/tutorial/views.py + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + +#. If you are still logged in, click the "Log Out" link. + +#. Visit http://localhost:6543/howdy in a browser. You should be asked to + login. + + +Analysis +======== + +This simple tutorial step can be boiled down to the following: + +- A view can require a *permission* (``edit``). + +- The context for our view (the ``Root``) has an access control list (ACL). + +- This ACL says that the ``edit`` permission is available on ``Root`` to the + ``group:editors`` *principal*. + +- The registered ``groupfinder`` answers whether a particular user (``editor``) + has a particular group (``group:editors``). + +In summary, ``hello`` wants ``edit`` permission, ``Root`` says +``group:editors`` has ``edit`` permission. + +Of course, this only applies on ``Root``. Some other part of the site (a.k.a. +*context*) might have a different ACL. + +If you are not logged in and visit ``/howdy``, you need to get shown the login +screen. How does Pyramid know what is the login page to use? We explicitly told +Pyramid that the ``login`` view should be used by decorating the view with +``@forbidden_view_config``. + + +Extra credit +============ + +#. Do I have to put a ``renderer`` in my ``@forbidden_view_config`` decorator? + +#. Perhaps you would like the experience of not having enough permissions + (forbidden) to be richer. How could you change this? + +#. Perhaps we want to store security statements in a database and allow editing + via a browser. How might this be done? + +#. What if we want different security statements on different kinds of objects? + Or on the same kinds of objects, but in different parts of a URL hierarchy? diff --git a/docs/quick_tutorial/authorization/development.ini b/docs/quick_tutorial/authorization/development.ini new file mode 100644 index 000000000..8a39b2fe7 --- /dev/null +++ b/docs/quick_tutorial/authorization/development.ini @@ -0,0 +1,11 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/authorization/setup.py b/docs/quick_tutorial/authorization/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/authorization/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/__init__.py b/docs/quick_tutorial/authorization/tutorial/__init__.py new file mode 100644 index 000000000..8f7ab8277 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/__init__.py @@ -0,0 +1,26 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings, + root_factory='.resources.Root') + config.include('pyramid_chameleon') + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/home.pt b/docs/quick_tutorial/authorization/tutorial/home.pt new file mode 100644 index 000000000..ed911b673 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/home.pt @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> + +<div> + <a tal:condition="view.logged_in is None" + href="${request.application_url}/login">Log In</a> + <a tal:condition="view.logged_in is not None" + href="${request.application_url}/logout">Logout</a> +</div> + +<h1>Hi ${name}</h1> +<p>Visit <a href="${request.route_url('hello')}">hello</a></p> +</body> +</html> diff --git a/docs/quick_tutorial/authorization/tutorial/login.pt b/docs/quick_tutorial/authorization/tutorial/login.pt new file mode 100644 index 000000000..9e5bfe2ad --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/login.pt @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Login</h1> +<span tal:replace="message"/> + +<form action="${url}" method="post"> + <input type="hidden" name="came_from" + value="${came_from}"/> + <label for="login">Username</label> + <input type="text" id="login" + name="login" + value="${login}"/><br/> + <label for="password">Password</label> + <input type="password" id="password" + name="password" + value="${password}"/><br/> + <input type="submit" name="form.submitted" + value="Log In"/> +</form> +</body> +</html> diff --git a/docs/quick_tutorial/authorization/tutorial/resources.py b/docs/quick_tutorial/authorization/tutorial/resources.py new file mode 100644 index 000000000..0cb656f12 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/resources.py @@ -0,0 +1,9 @@ +from pyramid.security import Allow, Everyone + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/security.py b/docs/quick_tutorial/authorization/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, [])
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py new file mode 100644 index 000000000..43d14455a --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -0,0 +1,66 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + +from pyramid.view import ( + view_config, + view_defaults, + forbidden_view_config + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = request.authenticated_userid + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello', permission='edit') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + @forbidden_view_config(renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/databases.rst b/docs/quick_tutorial/databases.rst new file mode 100644 index 000000000..c8d87c180 --- /dev/null +++ b/docs/quick_tutorial/databases.rst @@ -0,0 +1,198 @@ +.. _qtut_databases: + +============================== +19: Databases Using SQLAlchemy +============================== + +Store and retrieve data using the SQLAlchemy ORM atop the SQLite database. + + +Background +========== + +Our Pyramid-based wiki application now needs database-backed storage of pages. +This frequently means an SQL database. The Pyramid community strongly supports +the :ref:`SQLAlchemy <sqla:index_toplevel>` project and its +:ref:`object-relational mapper (ORM) <sqla:ormtutorial_toplevel>` as a +convenient, Pythonic way to interface to databases. + +In this step we hook up SQLAlchemy to a SQLite database table, providing +storage and retrieval for the wiki pages in the previous step. + +.. note:: + + The ``alchemy`` scaffold is really helpful for getting an SQLAlchemy + project going, including generation of the console script. Since we want to + see all the decisions, we will forgo convenience in this tutorial, and wire + it up ourselves. + + +Objectives +========== + +- Store pages in SQLite by using SQLAlchemy models. + +- Use SQLAlchemy queries to list/add/view/edit pages. + +- Provide a database-initialize command by writing a Pyramid *console script* + which can be run from the command line. + + +Steps +===== + +#. We are going to use the forms step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r forms databases; cd databases + +#. We need to add some dependencies in ``databases/setup.py`` as well as an + "entry point" for the command-line script: + + .. literalinclude:: databases/setup.py + :linenos: + + .. note:: + + We aren't yet doing ``$VENV/bin/pip install -e .`` as we will change it + later. + +#. Our configuration file at ``databases/development.ini`` wires together some + new pieces: + + .. literalinclude:: databases/development.ini + :language: ini + +#. This engine configuration now needs to be read into the application through + changes in ``databases/tutorial/__init__.py``: + + .. literalinclude:: databases/tutorial/__init__.py + :linenos: + +#. Make a command-line script at ``databases/tutorial/initialize_db.py`` to + initialize the database: + + .. literalinclude:: databases/tutorial/initialize_db.py + :linenos: + +#. Since ``setup.py`` changed, we now run it: + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + +#. The script references some models in ``databases/tutorial/models.py``: + + .. literalinclude:: databases/tutorial/models.py + :linenos: + +#. Let's run this console script, thus producing our database and table: + + .. code-block:: bash + + $ $VENV/bin/initialize_tutorial_db development.ini + + 2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-04-16 13:01:33,055 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-04-16 13:01:33,056 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages") + 2016-04-16 13:01:33,057 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread] + CREATE TABLE wikipages ( + uid INTEGER NOT NULL, + title TEXT, + body TEXT, + PRIMARY KEY (uid), + UNIQUE (title) + ) + + + 2016-04-16 13:01:33,058 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2016-04-16 13:01:33,059 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT + 2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit) + 2016-04-16 13:01:33,062 INFO [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO wikipages (title, body) VALUES (?, ?) + 2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] ('Root', '<p>Root</p>') + 2016-04-16 13:01:33,063 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT + +#. With our data now driven by SQLAlchemy queries, we need to update our + ``databases/tutorial/views.py``: + + .. literalinclude:: databases/tutorial/views.py + :linenos: + +#. Our tests in ``databases/tutorial/tests.py`` changed to include SQLAlchemy + bootstrapping: + + .. literalinclude:: databases/tutorial/tests.py + :linenos: + +#. Run the tests in your package using ``py.test``: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .. + 2 passed in 1.41 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + + +Analysis +======== + +Let's start with the dependencies. We made the decision to use ``SQLAlchemy`` +to talk to our database. We also, though, installed ``pyramid_tm`` and +``zope.sqlalchemy``. Why? + +Pyramid has a strong orientation towards support for ``transactions``. +Specifically, you can install a transaction manager into your application +either as middleware or a Pyramid "tween". Then, just before you return the +response, all transaction-aware parts of your application are executed. + +This means Pyramid view code usually doesn't manage transactions. If your view +code or a template generates an error, the transaction manager aborts the +transaction. This is a very liberating way to write code. + +The ``pyramid_tm`` package provides a "tween" that is configured in the +``development.ini`` configuration file. That installs it. We then need a +package that makes SQLAlchemy, and thus the RDBMS transaction manager, +integrate with the Pyramid transaction manager. That's what ``zope.sqlalchemy`` +does. + +Where do we point at the location on disk for the SQLite file? In the +configuration file. This lets consumers of our package change the location in a +safe (non-code) way. That is, in configuration. This configuration-oriented +approach isn't required in Pyramid; you can still make such statements in your +``__init__.py`` or some companion module. + +The ``initialize_tutorial_db`` is a nice example of framework support. You +point your setup at the location of some ``[console_scripts]``, and these get +generated into your virtual environment's ``bin`` directory. Our console script +follows the pattern of being fed a configuration file with all the +bootstrapping. It then opens SQLAlchemy and creates the root of the wiki, which +also makes the SQLite file. Note the ``with transaction.manager`` part that +puts the work in the scope of a transaction, as we aren't inside a web request +where this is done automatically. + +The ``models.py`` does a little bit of extra work to hook up SQLAlchemy into +the Pyramid transaction manager. It then declares the model for a ``Page``. + +Our views have changes primarily around replacing our dummy +dictionary-of-dictionaries data with proper database support: list the rows, +add a row, edit a row, and delete a row. + + +Extra credit +============ + +#. Why all this code? Why can't I just type two lines and have magic ensue? + +#. Give a try at a button that deletes a wiki page. diff --git a/docs/quick_tutorial/databases/development.ini b/docs/quick_tutorial/databases/development.ini new file mode 100644 index 000000000..5da87d602 --- /dev/null +++ b/docs/quick_tutorial/databases/development.ini @@ -0,0 +1,49 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/sqltutorial.sqlite + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial, sqlalchemy.engine.base.Engine + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_sqlalchemy.engine.base.Engine] +level = INFO +handlers = +qualname = sqlalchemy.engine.base.Engine + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/databases/setup.py b/docs/quick_tutorial/databases/setup.py new file mode 100644 index 000000000..238358fe4 --- /dev/null +++ b/docs/quick_tutorial/databases/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform', + 'sqlalchemy', + 'pyramid_tm', + 'zope.sqlalchemy' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.initialize_db:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/__init__.py b/docs/quick_tutorial/databases/tutorial/__init__.py new file mode 100644 index 000000000..74aa25740 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/__init__.py @@ -0,0 +1,21 @@ +from pyramid.config import Configurator + +from sqlalchemy import engine_from_config + +from .models import DBSession, Base + +def main(global_config, **settings): + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + + config = Configurator(settings=settings, + root_factory='tutorial.models.Root') + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/initialize_db.py b/docs/quick_tutorial/databases/tutorial/initialize_db.py new file mode 100644 index 000000000..98be524a1 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/initialize_db.py @@ -0,0 +1,37 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from .models import ( + DBSession, + Page, + Base, + ) + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri>\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) != 2: + usage(argv) + config_uri = argv[1] + setup_logging(config_uri) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page(title='Root', body='<p>Root</p>') + DBSession.add(model) diff --git a/docs/quick_tutorial/databases/tutorial/models.py b/docs/quick_tutorial/databases/tutorial/models.py new file mode 100644 index 000000000..b27c38417 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/models.py @@ -0,0 +1,35 @@ +from pyramid.security import Allow, Everyone + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session( + sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class Page(Base): + __tablename__ = 'wikipages' + uid = Column(Integer, primary_key=True) + title = Column(Text, unique=True) + body = Column(Text) + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/tests.py b/docs/quick_tutorial/databases/tutorial/tests.py new file mode 100644 index 000000000..11e747d15 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/tests.py @@ -0,0 +1,56 @@ +import unittest +import transaction + +from pyramid import testing + + +def _initTestingDB(): + from sqlalchemy import create_engine + from .models import ( + DBSession, + Page, + Base + ) + engine = create_engine('sqlite://') + Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page(title='FrontPage', body='This is the front page') + DBSession.add(model) + return DBSession + + +class WikiViewTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def test_wiki_view(self): + from tutorial.views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(response['title'], 'Wiki View') + + +class WikiFunctionalTests(unittest.TestCase): + def setUp(self): + from pyramid.paster import get_app + app = get_app('development.ini') + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + from .models import DBSession + DBSession.remove() + + def test_it(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'Wiki: View', res.body) + res = self.testapp.get('/add', status=200) + self.assertIn(b'Add/Edit', res.body) diff --git a/docs/quick_tutorial/databases/tutorial/views.py b/docs/quick_tutorial/databases/tutorial/views.py new file mode 100644 index 000000000..4608c6d43 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from .models import DBSession, Page + + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + pages = DBSession.query(Page).order_by(Page.title) + return dict(title='Wiki View', pages=pages) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Add a new page to the database + new_title = appstruct['title'] + new_body = appstruct['body'] + DBSession.add(Page(title=new_title, body=new_body)) + + # Get the new ID and redirect + page = DBSession.query(Page).filter_by(title=new_title).one() + new_uid = page.uid + + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + return dict(page=page) + + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page.title = appstruct['title'] + page.body = appstruct['body'] + url = self.request.route_url('wikipage_view', uid=uid) + return HTTPFound(url) + + form = self.wiki_form.render(dict( + uid=page.uid, title=page.title, body=page.body) + ) + + return dict(page=page, form=form) diff --git a/docs/quick_tutorial/databases/tutorial/wiki_view.pt b/docs/quick_tutorial/databases/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<p>${structure: form}</p> +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_view.pt b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst new file mode 100644 index 000000000..aaf904390 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar.rst @@ -0,0 +1,116 @@ +.. _qtut_debugtoolbar: + +============================================ +04: Easier Development with ``debugtoolbar`` +============================================ + +Error handling and introspection using the ``pyramid_debugtoolbar`` add-on. + + +Background +========== + +As we introduce the basics, we also want to show how to be productive in +development and debugging. For example, we just discussed template reloading, +and earlier we showed ``--reload`` for application reloading. + +``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes several tools +available in your browser. Adding it to your project illustrates several points +about configuration. + + +Objectives +========== + +- Install and enable the toolbar to help during development. + +- Explain Pyramid add-ons. + +- Show how an add-on gets configured into your application. + + +Steps +===== + +#. First we copy the results of the previous step, as well as install the + ``pyramid_debugtoolbar`` package: + + .. code-block:: bash + + $ cd ..; cp -r ini debugtoolbar; cd debugtoolbar + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install pyramid_debugtoolbar + +#. Our ``debugtoolbar/development.ini`` gets a configuration entry for + ``pyramid.includes``: + + .. literalinclude:: debugtoolbar/development.ini + :language: ini + :linenos: + +#. Run the WSGI application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. See the handy + toolbar on the right. + + +Analysis +======== + +``pyramid_debugtoolbar`` is a full-fledged Python package, available on PyPI +just like thousands of other Python packages. Thus we start by installing the +``pyramid_debugtoolbar`` package into our virtual environment using normal +Python package installation commands. + +The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, which +means we need to include its add-on configuration into our web application. We +could do this with imperative configuration in ``tutorial/__init__.py`` by +using ``config.include``. Pyramid also supports wiring in add-on configuration +via our ``development.ini`` using ``pyramid.includes``. We use this to load the +configuration for the debugtoolbar. + +You'll now see an attractive button on the right side of your browser, which +you may click to provide introspective access to debugging information in a new +rowser tab. Even better, if your web application generates an error, you will +see a nice traceback on the screen. When you want to disable this toolbar, +there's no need to change code: you can remove it from ``pyramid.includes`` in +the relevant ``.ini`` configuration file (thus showing why configuration files +are handy). + +Note that the toolbar injects a small amount of HTML/CSS into your app just +before the closing ``</body>`` tag in order to display itself. If you start to +experience otherwise inexplicable client-side weirdness, you can shut it off +by commenting out the ``pyramid_debugtoolbar`` line in ``pyramid.includes`` +temporarily. + +.. seealso:: See also :ref:`pyramid_debugtoolbar <toolbar:overview>`. + + +Extra Credit +============ + +#. Why don't we add ``pyramid_debugtoolbar`` to the list of + ``install_requires`` dependencies in ``debugtoolbar/setup.py``? + +#. Introduce a bug into your application. Change: + + .. code-block:: python + + def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + to: + + .. code-block:: python + + def hello_world(request): + return xResponse('<body><h1>Hello World!</h1></body>') + + Save, and visit http://localhost:6543/ again. Notice the nice traceback + display. On the lowest line, click the "screen" icon to the right, and try + typing the variable names ``request`` and ``Response``. What else can you + discover? diff --git a/docs/quick_tutorial/debugtoolbar/development.ini b/docs/quick_tutorial/debugtoolbar/development.ini new file mode 100644 index 000000000..52b2a3a41 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/development.ini @@ -0,0 +1,9 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/debugtoolbar/setup.py b/docs/quick_tutorial/debugtoolbar/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py new file mode 100644 index 000000000..d784292ee --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst new file mode 100644 index 000000000..6b29833bd --- /dev/null +++ b/docs/quick_tutorial/forms.rst @@ -0,0 +1,153 @@ +.. _qtut_forms: + +==================================== +18: Forms and Validation With Deform +==================================== + +Schema-driven, autogenerated forms with validation. + + +Background +========== + +Modern web applications deal extensively with forms. Developers, though, have a +wide range of philosophies about how frameworks should help them with their +forms. As such, Pyramid doesn't directly bundle one particular form library. +Instead there are a variety of form libraries that are easy to use in Pyramid. + +:ref:`Deform <deform:overview>` is one such library. In this step, we introduce +Deform for our forms. This also gives us :ref:`Colander <colander:overview>` +for schemas and validation. + +Deform uses styling from Twitter Bootstrap and advanced widgets from popular +JavaScript projects. + + +Objectives +========== + +- Make a schema using Colander, the companion to Deform. + +- Create a form with Deform and change our views to handle validation. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes forms; cd forms + +#. Let's edit ``forms/setup.py`` to declare a dependency on Deform (which then + pulls in Colander as a dependency: + + .. literalinclude:: forms/setup.py + :linenos: + +#. We can now install our project in development mode: + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + +#. Register a static view in ``forms/tutorial/__init__.py`` for Deform's CSS, + JavaScript, etc., as well as our demo wiki page's views: + + .. literalinclude:: forms/tutorial/__init__.py + :linenos: + +#. Implement the new views, as well as the form schemas and some dummy data, in + ``forms/tutorial/views.py``: + + .. literalinclude:: forms/tutorial/views.py + :linenos: + +#. A template for the top of the "wiki" in ``forms/tutorial/wiki_view.pt``: + + .. literalinclude:: forms/tutorial/wiki_view.pt + :language: html + :linenos: + +#. Another template for adding/editing in + ``forms/tutorial/wikipage_addedit.pt``: + + .. literalinclude:: forms/tutorial/wikipage_addedit.pt + :language: html + :linenos: + +#. Finally, a template at ``forms/tutorial/wikipage_view.pt`` for viewing a + wiki page: + + .. literalinclude:: forms/tutorial/wikipage_view.pt + :language: html + :linenos: + +#. Run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .. + 2 passed in 0.45 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + + +Analysis +======== + +This step helps illustrate the utility of asset specifications for static +assets. We have an outside package called Deform with static assets which need +to be published. We don't have to know where on disk it is located. We point at +the package, then the path inside the package. + +We just need to include a call to ``add_static_view`` to make that directory +available at a URL. For Pyramid-specific packages, Pyramid provides a facility +(``config.include()``) which even makes that unnecessary for consumers of a +package. (Deform is not specific to Pyramid.) + +Our forms have rich widgets which need the static CSS and JavaScript just +mentioned. Deform has a :term:`resource registry` which allows widgets to +specify which JavaScript and CSS are needed. Our ``wikipage_addedit.pt`` +template shows how we iterated over that data to generate markup that includes +the needed resources. + +Our add and edit views use a pattern called *self-posting forms*. Meaning, the +same URL is used to ``GET`` the form as is used to ``POST`` the form. The +route, the view, and the template are the same URL whether you are walking up +to it for the first time or you clicked a button. + +Inside the view we do ``if 'submit' in self.request.params:`` to see if this +form was a ``POST`` where the user clicked on a particular button +``<input name="submit">``. + +The form controller then follows a typical pattern: + +- If you are doing a ``GET``, skip over and just return the form. + +- If you are doing a ``POST``, validate the form contents. + +- If the form is invalid, bail out by re-rendering the form with the supplied + ``POST`` data. + +- If the validation succeeded, perform some action and issue a redirect via + ``HTTPFound``. + +We are, in essence, writing our own form controller. Other Pyramid-based +systems, including ``pyramid_deform``, provide a form-centric view class which +automates much of this branching and routing. + + +Extra credit +============ + +#. Give a try at a button that goes to a delete view for a particular wiki + page. diff --git a/docs/quick_tutorial/forms/development.ini b/docs/quick_tutorial/forms/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/forms/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/forms/setup.py b/docs/quick_tutorial/forms/setup.py new file mode 100644 index 000000000..361ade013 --- /dev/null +++ b/docs/quick_tutorial/forms/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/__init__.py b/docs/quick_tutorial/forms/tutorial/__init__.py new file mode 100644 index 000000000..dff7457cf --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/forms/tutorial/tests.py b/docs/quick_tutorial/forms/tutorial/tests.py new file mode 100644 index 000000000..5a2c40904 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(len(response['pages']), 3) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<title>Wiki: View</title>', res.body) diff --git a/docs/quick_tutorial/forms/tutorial/views.py b/docs/quick_tutorial/forms/tutorial/views.py new file mode 100644 index 000000000..004d2aba9 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +pages = { + '100': dict(uid='100', title='Page 100', body='<em>100</em>'), + '101': dict(uid='101', title='Page 101', body='<em>101</em>'), + '102': dict(uid='102', title='Page 102', body='<em>102</em>') +} + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + return dict(pages=pages.values()) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Form is valid, make a new identifier and add to list + last_uid = int(sorted(pages.keys())[-1]) + new_uid = str(last_uid + 1) + pages[new_uid] = dict( + uid=new_uid, title=appstruct['title'], + body=appstruct['body'] + ) + + # Now visit new page + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + return dict(page=page) + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + + url = self.request.route_url('wikipage_view', + uid=page['uid']) + return HTTPFound(url) + + form = wiki_form.render(page) + + return dict(page=page, form=form)
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wiki_view.pt b/docs/quick_tutorial/forms/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<p>${structure: form}</p> +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_view.pt b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst new file mode 100644 index 000000000..33793578a --- /dev/null +++ b/docs/quick_tutorial/functional_testing.rst @@ -0,0 +1,73 @@ +.. _qtut_functional_testing: + +=================================== +06: Functional Testing with WebTest +=================================== + +Write end-to-end full-stack testing using ``webtest``. + + +Background +========== + +Unit tests are a common and popular approach to test-driven development (TDD). +In web applications, though, the templating and entire apparatus of a web site +are important parts of the delivered quality. We'd like a way to test these. + +`WebTest <http://docs.pylonsproject.org/projects/webtest/en/latest/>`_ is a +Python package that does functional testing. With WebTest you can write tests +which simulate a full HTTP request against a WSGI application, then test the +information in the response. For speed purposes, WebTest skips the +setup/teardown of an actual HTTP server, providing tests that run fast enough +to be part of TDD. + + +Objectives +========== + +- Write a test which checks the contents of the returned HTML. + + +Steps +===== + +#. First we copy the results of the previous step, as well as install the + ``webtest`` package: + + .. code-block:: bash + + $ cd ..; cp -r unit_testing functional_testing; cd functional_testing + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install webtest + +#. Let's extend ``functional_testing/tutorial/tests.py`` to include a + functional test: + + .. literalinclude:: functional_testing/tutorial/tests.py + :linenos: + + Be sure this file is not executable, or ``pytest`` may not include your + tests. + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .. + 2 passed in 0.25 seconds + + +Analysis +======== + +We now have the end-to-end testing we were looking for. WebTest lets us simply +extend our existing ``pytest``-based test approach with functional tests that +are reported in the same output. These new tests not only cover our templating, +but they didn't dramatically increase the execution time of our tests. + + +Extra credit +============ + +#. Why do our functional tests use ``b''``? diff --git a/docs/quick_tutorial/functional_testing/development.ini b/docs/quick_tutorial/functional_testing/development.ini new file mode 100644 index 000000000..52b2a3a41 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/development.ini @@ -0,0 +1,9 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/functional_testing/setup.py b/docs/quick_tutorial/functional_testing/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/__init__.py b/docs/quick_tutorial/functional_testing/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/tests.py b/docs/quick_tutorial/functional_testing/tutorial/tests.py new file mode 100644 index 000000000..4248acbe7 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_hello_world(self): + from tutorial import hello_world + + request = testing.DummyRequest() + response = hello_world(request) + self.assertEqual(response.status_code, 200) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_hello_world(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hello World!</h1>', res.body) diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst new file mode 100644 index 000000000..4e35da7bb --- /dev/null +++ b/docs/quick_tutorial/hello_world.rst @@ -0,0 +1,113 @@ +.. _qtut_hello_world: + +================================ +01: Single-File Web Applications +================================ + +What's the simplest way to get started in Pyramid? A single-file module. No +Python packages, no ``pip install -e .``, no other machinery. + + +Background +========== + +Microframeworks are all the rage these days. "Microframework" is a marketing +term, not a technical one. They have a low mental overhead: they do so little, +the only things you have to worry about are *your things*. + +Pyramid is special because it can act as a single-file module microframework. +You can have a single Python file that can be executed directly by Python. But +Pyramid also provides facilities to scale to the largest of applications. + +Python has a standard called :term:`WSGI` that defines how Python web +applications plug into standard servers, getting passed incoming requests, and +returning responses. Most modern Python web frameworks obey an "MVC" +(model-view-controller) application pattern, where the data in the model has a +view that mediates interaction with outside systems. + +In this step we'll see a brief glimpse of WSGI servers, WSGI applications, +requests, responses, and views. + + +Objectives +========== + +- Get a running Pyramid web application, as simply as possible. + +- Use that as a well-understood base for adding each unit of complexity. + +- Initial exposure to WSGI apps, requests, views, and responses. + + +Steps +===== + +#. Make sure you have followed the steps in :doc:`requirements`. + +#. Starting from your workspace directory + (``~/projects/quick_tutorial``), create a directory for this step: + + .. code-block:: bash + + $ mkdir hello_world; cd hello_world + +#. Copy the following into ``hello_world/app.py``: + + .. literalinclude:: hello_world/app.py + :linenos: + +#. Run the application: + + .. code-block:: bash + + $ $VENV/bin/python app.py + +#. Open http://localhost:6543/ in your browser. + + +Analysis +======== + +New to Python web programming? If so, some lines in the module merit +explanation: + +#. *Line 11*. The ``if __name__ == '__main__':`` is Python's way of saying, + "Start here when running from the command line", rather than when this + module is imported. + +#. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect :term:`view` + code to a particular URL :term:`route`. + +#. *Lines 6-8*. Implement the view code that generates the :term:`response`. + +#. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP server. + +As shown in this example, the :term:`configurator` plays a central role in +Pyramid development. Building an application from loosely-coupled parts via +:ref:`configuration_narr` is a central idea in Pyramid, one that we will +revisit regularly in this *Quick Tutorial*. + + +Extra Credit +============ + +#. Why do we do this: + + .. code-block:: python + + print('Incoming request') + + ...instead of: + + .. code-block:: python + + print 'Incoming request' + +#. What happens if you return a string of HTML? A sequence of integers? + +#. Put something invalid, such as ``print xyz``, in the view function. Kill + your ``python app.py`` with ``ctrl-C`` and restart, then reload your + browser. See the exception in the console? + +#. The ``GI`` in ``WSGI`` stands for "Gateway Interface". What web standard is + this modelled after? diff --git a/docs/quick_tutorial/hello_world/app.py b/docs/quick_tutorial/hello_world/app.py new file mode 100644 index 000000000..0a95f9ad3 --- /dev/null +++ b/docs/quick_tutorial/hello_world/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print('Incoming request') + return Response('<body><h1>Hello World!</h1></body>') + + +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() diff --git a/docs/quick_tutorial/index.rst b/docs/quick_tutorial/index.rst new file mode 100644 index 000000000..29b4d8fb7 --- /dev/null +++ b/docs/quick_tutorial/index.rst @@ -0,0 +1,50 @@ +.. _quick_tutorial: + +========================== +Quick Tutorial for Pyramid +========================== + +Pyramid is a web framework for Python 2 and 3. This tutorial gives a Python +3/2-compatible, high-level tour of the major features. + +This hands-on tutorial covers "a little about a lot": practical introductions +to the most common facilities. Fun, fast-paced, and most certainly not aimed at +experts of the Pyramid web framework. + +Contents +======== + +.. toctree:: + :maxdepth: 1 + + requirements + tutorial_approach + scaffolds + hello_world + package + ini + debugtoolbar + unit_testing + functional_testing + views + templating + view_classes + request_response + routing + jinja2 + static_assets + json + more_view_classes + logging + sessions + forms + databases + authentication + authorization + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst new file mode 100644 index 000000000..fba5ce29e --- /dev/null +++ b/docs/quick_tutorial/ini.rst @@ -0,0 +1,142 @@ +.. _qtut_ini: + +================================================= +03: Application Configuration with ``.ini`` Files +================================================= + +Use Pyramid's ``pserve`` command with a ``.ini`` configuration file for +simpler, better application running. + + +Background +========== + +Pyramid has a first-class concept of :ref:`configuration <configuration_narr>` +distinct from code. This approach is optional, but its presence makes it +distinct from other Python web frameworks. It taps into Python's ``setuptools`` +library, which establishes conventions for installing and providing "entry +points" for Python projects. Pyramid uses an entry point to let a Pyramid +application know where to find the WSGI app. + + +Objectives +========== + +- Modify our ``setup.py`` to have an entry point telling Pyramid the location + of the WSGI app. + +- Create an application driven by an ``.ini`` file. + +- Start the application with Pyramid's ``pserve`` command. + +- Move code into the package's ``__init__.py``. + + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r package ini; cd ini + +#. Our ``ini/setup.py`` needs a setuptools "entry point" in the ``setup()`` + function: + + .. literalinclude:: ini/setup.py + :linenos: + +#. We can now install our project, thus generating (or re-generating) an "egg" + at ``ini/tutorial.egg-info``: + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + +#. Let's make a file ``ini/development.ini`` for our configuration: + + .. literalinclude:: ini/development.ini + :language: ini + :linenos: + +#. We can refactor our startup code from the previous step's ``app.py`` into + ``ini/tutorial/__init__.py``: + + .. literalinclude:: ini/tutorial/__init__.py + :linenos: + +#. Now that ``ini/tutorial/app.py`` isn't used, let's remove it: + + .. code-block:: bash + + $ rm tutorial/app.py + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/. + +Analysis +======== + +Our ``development.ini`` file is read by ``pserve`` and serves to bootstrap our +application. Processing then proceeds as described in the Pyramid chapter on +:ref:`application startup <startup_chapter>`: + +- ``pserve`` looks for ``[app:main]`` and finds ``use = egg:tutorial``. + +- The projects's ``setup.py`` has defined an "entry point" (lines 9-12) for the + project's "main" entry point of ``tutorial:main``. + +- The ``tutorial`` package's ``__init__`` has a ``main`` function. + +- This function is invoked, with the values from certain ``.ini`` sections + passed in. + +The ``.ini`` file is also used for two other functions: + +- *Configuring the WSGI server*. ``[server:main]`` wires up the choice of which + WSGI *server* for your WSGI *application*. In this case, we are using + ``wsgiref`` bundled in the Python library. It also wires up the *port + number*: ``port = 6543`` tells ``wsgiref`` to listen on port 6543. + +- *Configuring Python logging*. Pyramid uses Python standard logging, which + needs a number of configuration values. The ``.ini`` serves this function. + This provides the console log output that you see on startup and each + request. + +We moved our startup code from ``app.py`` to the package's +``tutorial/__init__.py``. This isn't necessary, but it is a common style in +Pyramid to take the WSGI app bootstrapping out of your module's code and put it +in the package's ``__init__.py``. + +The ``pserve`` application runner has a number of command-line arguments and +options. We are using ``--reload`` which tells ``pserve`` to watch the +filesystem for changes to relevant code (Python files, the INI file, etc.) and, +when something changes, restart the application. Very handy during development. + + +Extra Credit +============ + +#. If you don't like configuration and/or ``.ini`` files, could you do this + yourself in Python code? + +#. Can we have multiple ``.ini`` configuration files for a project? Why might + you want to do that? + +#. The entry point in ``setup.py`` didn't mention ``__init__.py`` when it + declared ``tutorial:main`` function. Why not? + +#. What is the purpose of ``**settings``? What does the ``**`` signify? + +.. seealso:: + :ref:`project_narr`, + :ref:`scaffolding_chapter`, + :ref:`what_is_this_pserve_thing`, + :ref:`environment_chapter`, + :ref:`paste_chapter` diff --git a/docs/quick_tutorial/ini/development.ini b/docs/quick_tutorial/ini/development.ini new file mode 100644 index 000000000..8853e2c2b --- /dev/null +++ b/docs/quick_tutorial/ini/development.ini @@ -0,0 +1,7 @@ +[app:main] +use = egg:tutorial + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/ini/setup.py b/docs/quick_tutorial/ini/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/ini/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/ini/tutorial/__init__.py b/docs/quick_tutorial/ini/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/ini/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst new file mode 100644 index 000000000..2fc68827b --- /dev/null +++ b/docs/quick_tutorial/jinja2.rst @@ -0,0 +1,91 @@ +.. _qtut_jinja2: + +============================== +12: Templating With ``jinja2`` +============================== + +We just said Pyramid doesn't prefer one templating language over another. Time +to prove it. Jinja2 is a popular templating system, used in Flask and modeled +after Django's templates. Let's add ``pyramid_jinja2``, a Pyramid +:term:`add-on` which enables Jinja2 as a :term:`renderer` in our Pyramid +applications. + + +Objectives +========== + +- Show Pyramid's support for different templating systems. + +- Learn about installing Pyramid add-ons. + + +Steps +===== + +#. In this step let's start by copying the ``view_class`` step's directory, + and then installing the ``pyramid_jinja2`` add-on. + + .. code-block:: bash + + $ cd ..; cp -r view_classes jinja2; cd jinja2 + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install pyramid_jinja2 + +#. We need to include ``pyramid_jinja2`` in ``jinja2/tutorial/__init__.py``: + + .. literalinclude:: jinja2/tutorial/__init__.py + :linenos: + +#. Our ``jinja2/tutorial/views.py`` simply changes its ``renderer``: + + .. literalinclude:: jinja2/tutorial/views.py + :linenos: + +#. Add ``jinja2/tutorial/home.jinja2`` as a template: + + .. literalinclude:: jinja2/tutorial/home.jinja2 + :language: html + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.40 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. + + +Analysis +======== + +Getting a Pyramid add-on into Pyramid is simple. First you use normal Python +package installation tools to install the add-on package into your Python +virtual environment. You then tell Pyramid's configurator to run the setup code +in the add-on. In this case the setup code told Pyramid to make a new +"renderer" available that looked for ``.jinja2`` file extensions. + +Our view code stayed largely the same. We simply changed the file extension on +the renderer. For the template, the syntax for Chameleon and Jinja2's basic +variable insertion is very similar. + + +Extra credit +============ + +#. Our project now depends on ``pyramid_jinja2``. We installed that dependency + manually. What is another way we could have made the association? + +#. We used ``config.include`` which is an imperative configuration to get the + :term:`Configurator` to load ``pyramid_jinja2``'s configuration. What is + another way could include it into the config? + +.. seealso:: `Jinja2 homepage <http://jinja.pocoo.org/>`_, and + :ref:`pyramid_jinja2 Overview <jinja2:overview>` diff --git a/docs/quick_tutorial/jinja2/development.ini b/docs/quick_tutorial/jinja2/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/jinja2/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/jinja2/setup.py b/docs/quick_tutorial/jinja2/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/jinja2/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/__init__.py b/docs/quick_tutorial/jinja2/tutorial/__init__.py new file mode 100644 index 000000000..1f6783c06 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/home.jinja2 b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 new file mode 100644 index 000000000..20d33b733 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: {{ name }}</title> +</head> +<body> +<h1>Hi {{ name }}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/jinja2/tutorial/tests.py b/docs/quick_tutorial/jinja2/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/jinja2/tutorial/views.py b/docs/quick_tutorial/jinja2/tutorial/views.py new file mode 100644 index 000000000..fa9ec5121 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/views.py @@ -0,0 +1,18 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.jinja2') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/json.rst b/docs/quick_tutorial/json.rst new file mode 100644 index 000000000..ff153d2b5 --- /dev/null +++ b/docs/quick_tutorial/json.rst @@ -0,0 +1,104 @@ +.. _qtut_json: + +======================================== +14: AJAX Development With JSON Renderers +======================================== + +Modern web apps are more than rendered HTML. Dynamic pages now use JavaScript +to update the UI in the browser by requesting server data as JSON. Pyramid +supports this with a *JSON renderer*. + + +Background +========== + +As we saw in :doc:`templating`, view declarations can specify a renderer. +Output from the view is then run through the renderer, which generates and +returns the response. We first used a Chameleon renderer, then a Jinja2 +renderer. + +Renderers aren't limited, however, to templates that generate HTML. Pyramid +supplies a JSON renderer which takes Python data, serializes it to JSON, and +performs some other functions such as setting the content type. In fact you can +write your own renderer (or extend a built-in renderer) containing custom logic +for your unique application. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes json; cd json + $ $VENV/bin/pip install -e . + +#. We add a new route for ``hello_json`` in ``json/tutorial/__init__.py``: + + .. literalinclude:: json/tutorial/__init__.py + :linenos: + +#. Rather than implement a new view, we will "stack" another decorator on the + ``hello`` view in ``views.py``: + + .. literalinclude:: json/tutorial/views.py + :linenos: + +#. We need a new functional test at the end of ``json/tutorial/tests.py``: + + .. literalinclude:: json/tutorial/tests.py + :linenos: + +#. Run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + ..... + 5 passed in 0.47 seconds + + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy.json in your browser and you will see the + resulting JSON response. + + +Analysis +======== + +Earlier we changed our view functions and methods to return Python data. This +change to a data-oriented view layer made test writing easier, decoupling the +templating from the view logic. + +Since Pyramid has a JSON renderer as well as the templating renderers, it is an +easy step to return JSON. In this case we kept the exact same view and arranged +to return a JSON encoding of the view data. We did this by: + +- Adding a route to map ``/howdy.json`` to a route name. + +- Providing a ``@view_config`` that associated that route name with an existing + view. + +- *Overriding* the view defaults in the view config that mentions the + ``hello_json`` route, so that when the route is matched, we use the JSON + renderer rather than the ``home.pt`` template renderer that would otherwise + be used. + +In fact, for pure AJAX-style web applications, we could re-use the existing +route by using Pyramid's view predicates to match on the ``Accepts:`` header +sent by modern AJAX implementations. + +Pyramid's JSON renderer uses the base Python JSON encoder, thus inheriting its +strengths and weaknesses. For example, Python can't natively JSON encode +DateTime objects. There are a number of solutions for this in Pyramid, +including extending the JSON renderer with a custom renderer. + +.. seealso:: :ref:`views_which_use_a_renderer`, + :ref:`json_renderer`, and + :ref:`adding_and_overriding_renderers` diff --git a/docs/quick_tutorial/json/development.ini b/docs/quick_tutorial/json/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/json/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/json/setup.py b/docs/quick_tutorial/json/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/json/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/__init__.py b/docs/quick_tutorial/json/tutorial/__init__.py new file mode 100644 index 000000000..6652544c3 --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/__init__.py @@ -0,0 +1,11 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('hello_json', 'howdy.json') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/home.pt b/docs/quick_tutorial/json/tutorial/home.pt new file mode 100644 index 000000000..fd4ef8764 --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/json/tutorial/tests.py b/docs/quick_tutorial/json/tutorial/tests.py new file mode 100644 index 000000000..c3cdacbdb --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/tests.py @@ -0,0 +1,50 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) + + def test_hello_json(self): + res = self.testapp.get('/howdy.json', status=200) + self.assertIn(b'{"name": "Hello View"}', res.body) + self.assertEqual(res.content_type, 'application/json') + diff --git a/docs/quick_tutorial/json/tutorial/views.py b/docs/quick_tutorial/json/tutorial/views.py new file mode 100644 index 000000000..f15e55d1b --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/views.py @@ -0,0 +1,19 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + @view_config(route_name='hello_json', renderer='json') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst new file mode 100644 index 000000000..cbbf7860e --- /dev/null +++ b/docs/quick_tutorial/logging.rst @@ -0,0 +1,86 @@ +.. _qtut_logging: + +============================================ +16: Collecting Application Info With Logging +============================================ + +Capture debugging and error output from your web applications using standard +Python logging. + + +Background +========== + +It's important to know what is going on inside our web application. In +development we might need to collect some output. In production, we might need +to detect problems when other people use the site. We need *logging*. + +Fortunately Pyramid uses the normal Python approach to logging. The scaffold +generated in your ``development.ini`` has a number of lines that configure the +logging for you to some reasonable defaults. You then see messages sent by +Pyramid, for example, when a new request comes in. + + +Objectives +========== + +- Inspect the configuration setup used for logging. + +- Add logging statements to your view code. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes logging; cd logging + $ $VENV/bin/pip install -e . + +#. Extend ``logging/tutorial/views.py`` to log a message: + + .. literalinclude:: logging/tutorial/views.py + :linenos: + +#. Finally let's edit ``development.ini`` configuration file to enable logging + for our Pyramid application: + + .. literalinclude:: logging/development.ini + :language: ini + +#. Make sure the tests still pass: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.41 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser. + Note, both in the console and in the debug toolbar, the message that you + logged. + + +Analysis +======== + +In our configuration file ``development.ini``, our ``tutorial`` Python package +is set up as a logger and configured to log messages at a ``DEBUG`` or higher +level. When you visit http://localhost:6543, your console will now show: + +.. code-block:: text + + 2013-08-09 10:42:42,968 DEBUG [tutorial.views][MainThread] In home view + +Also, if you have configured your Pyramid application to use the +``pyramid_debugtoolbar``, logging statements appear in one of its menus. + +.. seealso:: See also :ref:`logging_chapter`. diff --git a/docs/quick_tutorial/logging/development.ini b/docs/quick_tutorial/logging/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/logging/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/logging/setup.py b/docs/quick_tutorial/logging/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/logging/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/__init__.py b/docs/quick_tutorial/logging/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/home.pt b/docs/quick_tutorial/logging/tutorial/home.pt new file mode 100644 index 000000000..fd4ef8764 --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/logging/tutorial/tests.py b/docs/quick_tutorial/logging/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/logging/tutorial/views.py b/docs/quick_tutorial/logging/tutorial/views.py new file mode 100644 index 000000000..63d95f405 --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/views.py @@ -0,0 +1,23 @@ +import logging +log = logging.getLogger(__name__) + +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + log.debug('In home view') + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + log.debug('In hello view') + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/more_view_classes.rst b/docs/quick_tutorial/more_view_classes.rst new file mode 100644 index 000000000..30234ea2e --- /dev/null +++ b/docs/quick_tutorial/more_view_classes.rst @@ -0,0 +1,197 @@ +.. _qtut_more_view_classes: + +========================== +15: More With View Classes +========================== + +Group views into a class, sharing configuration, state, and logic. + + +Background +========== + +As part of its mission to help build more ambitious web applications, Pyramid +provides many more features for views and view classes. + +The Pyramid documentation discusses views as a Python "callable". This callable +can be a function, an object with a ``__call__``, or a Python class. In this +last case, methods on the class can be decorated with ``@view_config`` to +register the class methods with the :term:`configurator` as a view. + +At first, our views were simple, free-standing functions. Many times your views +are related: different ways to look at or work on the same data, or a REST API +that handles multiple operations. Grouping these together as a :ref:`view class +<class_as_view>` makes sense: + +- Group views. + +- Centralize some repetitive defaults. + +- Share some state and helpers. + +Pyramid views have :ref:`view predicates <view_configuration_parameters>` that +determine which view is matched to a request, based on factors such as the +request method, the form parameters, and so on. These predicates provide many +axes of flexibility. + +The following shows a simple example with four operations: view a home page +which leads to a form, save a change, and press the delete button. + + +Objectives +========== + +- Group related views into a view class. + +- Centralize configuration with class-level ``@view_defaults``. + +- Dispatch one route/URL to multiple views based on request data. + +- Share states and logic between views and templates via the view class. + + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r templating more_view_classes; cd more_view_classes + $ $VENV/bin/pip install -e . + +#. Our route in ``more_view_classes/tutorial/__init__.py`` needs some + replacement patterns: + + .. literalinclude:: more_view_classes/tutorial/__init__.py + :linenos: + +#. Our ``more_view_classes/tutorial/views.py`` now has a view class with + several views: + + .. literalinclude:: more_view_classes/tutorial/views.py + :linenos: + +#. Our primary view needs a template at ``more_view_classes/tutorial/home.pt``: + + .. literalinclude:: more_view_classes/tutorial/home.pt + :language: html + +#. Ditto for our other view from the previous section at + ``more_view_classes/tutorial/hello.pt``: + + .. literalinclude:: more_view_classes/tutorial/hello.pt + :language: html + +#. We have an edit view that also needs a template at + ``more_view_classes/tutorial/edit.pt``: + + .. literalinclude:: more_view_classes/tutorial/edit.pt + :language: html + +#. And finally the delete view's template at + ``more_view_classes/tutorial/delete.pt``: + + .. literalinclude:: more_view_classes/tutorial/delete.pt + :language: html + +#. Our tests in ``more_view_classes/tutorial/tests.py`` fail, so let's modify + them: + + .. literalinclude:: more_view_classes/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .. + 2 passed in 0.40 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy/jane/doe in your browser. Click the + ``Save`` and ``Delete`` buttons, and watch the output in the console window. + + +Analysis +======== + +As you can see, the four views are logically grouped together. Specifically: + +- We have a ``home`` view available at http://localhost:6543/ with a clickable + link to the ``hello`` view. + +- The second view is returned when you go to ``/howdy/jane/doe``. This URL is + mapped to the ``hello`` route that we centrally set using the optional + ``@view_defaults``. + +- The third view is returned when the form is submitted with a ``POST`` method. + This rule is specified in the ``@view_config`` for that view. + +- The fourth view is returned when clicking on a button such as ``<input + type="submit" name="form.delete" value="Delete"/>``. + +In this step we show, using the following information as criteria, how to +decide which view to use: + +- Method of the HTTP request (``GET``, ``POST``, etc.) + +- Parameter information in the request (submitted form field names) + +We also centralize part of the view configuration to the class level with +``@view_defaults``, then in one view, override that default just for that one +view. Finally, we put this commonality between views to work in the view class +by sharing: + +- State assigned in ``TutorialViews.__init__`` + +- A computed value + +These are then available both in the view methods and in the templates (e.g., +``${view.view_name}`` and ``${view.full_name}``). + +As a note, we made a switch in our templates on how we generate URLs. We +previously hardcoded the URLs, such as: + +.. code-block:: html + + <a href="/howdy/jane/doe">Howdy</a> + +In ``home.pt`` we switched to: + +.. code-block:: xml + + <a href="${request.route_url('hello', first='jane', + last='doe')}">form</a> + +Pyramid has rich facilities to help generate URLs in a flexible, non-error +prone fashion. + +Extra credit +============ + +#. Why could our template do ``${view.full_name}`` and not have to do + ``${view.full_name()}``? + +#. The ``edit`` and ``delete`` views are both receive ``POST`` requests. Why + does the ``edit`` view configuration not catch the ``POST`` used by + ``delete``? + +#. We used Python ``@property`` on ``full_name``. If we reference this many + times in a template or view code, it would re-compute this every time. Does + Pyramid provide something that will cache the initial computation on a + property? + +#. Can you associate more than one route with the same view? + +#. There is also a ``request.route_path`` API. How does this differ from + ``request.route_url``? + +.. seealso:: :ref:`class_as_view`, `Weird Stuff You Can Do With + URL Dispatch <http://www.plope.com/weird_pyramid_urldispatch>`_ diff --git a/docs/quick_tutorial/more_view_classes/development.ini b/docs/quick_tutorial/more_view_classes/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/more_view_classes/setup.py b/docs/quick_tutorial/more_view_classes/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/__init__.py b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py new file mode 100644 index 000000000..9c1bcec06 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy/{first}/{last}') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/delete.pt b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt new file mode 100644 index 000000000..7bd4d3b0d --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/more_view_classes/tutorial/edit.pt b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt new file mode 100644 index 000000000..523a4ce5d --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +<p>You submitted <code>${new_name}</code></p> +</body> +</html> diff --git a/docs/quick_tutorial/more_view_classes/tutorial/hello.pt b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt new file mode 100644 index 000000000..40b00bfe4 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +<p>Welcome, ${view.full_name}</p> +<form method="POST" + action="${request.current_route_url()}"> + <input name="new_name"/> + <input type="submit" name="form.edit" value="Save"/> + <input type="submit" name="form.delete" value="Delete"/> +</form> +</body> +</html> diff --git a/docs/quick_tutorial/more_view_classes/tutorial/home.pt b/docs/quick_tutorial/more_view_classes/tutorial/home.pt new file mode 100644 index 000000000..fa0436f7e --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/home.pt @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> + +<p>Go to the <a href="${request.route_url('hello', first='jane', + last='doe')}">form</a>.</p> +</body> +</html> diff --git a/docs/quick_tutorial/more_view_classes/tutorial/tests.py b/docs/quick_tutorial/more_view_classes/tutorial/tests.py new file mode 100644 index 000000000..dca8d7f7b --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['page_title']) + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'TutorialViews - Home View', res.body) diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py new file mode 100644 index 000000000..156e468a9 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py @@ -0,0 +1,39 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(route_name='hello') +class TutorialViews(object): + def __init__(self, request): + self.request = request + self.view_name = 'TutorialViews' + + @property + def full_name(self): + first = self.request.matchdict['first'] + last = self.request.matchdict['last'] + return first + ' ' + last + + @view_config(route_name='home', renderer='home.pt') + def home(self): + return {'page_title': 'Home View'} + + # Retrieving /howdy/first/last the first time + @view_config(renderer='hello.pt') + def hello(self): + return {'page_title': 'Hello View'} + + # Posting to /howdy/first/last via the "Edit" submit button + @view_config(request_method='POST', renderer='edit.pt') + def edit(self): + new_name = self.request.params['new_name'] + return {'page_title': 'Edit View', 'new_name': new_name} + + # Posting to /howdy/first/last via the "Delete" submit button + @view_config(request_method='POST', request_param='form.delete', + renderer='delete.pt') + def delete(self): + print ('Deleted') + return {'page_title': 'Delete View'} diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst new file mode 100644 index 000000000..94cb39fc9 --- /dev/null +++ b/docs/quick_tutorial/package.rst @@ -0,0 +1,111 @@ +============================================ +02: Python Packages for Pyramid Applications +============================================ + +Most modern Python development is done using Python packages, an approach +Pyramid puts to good use. In this step we redo "Hello World" as a minimal +Python package inside a minimal Python project. + + +Background +========== + +Python developers can organize a collection of modules and files into a +namespaced unit called a :ref:`package <python:tut-packages>`. If a directory +is on ``sys.path`` and has a special file named ``__init__.py``, it is treated +as a Python package. + +Packages can be bundled up, made available for installation, and installed +through a toolchain oriented around a ``setup.py`` file. For this tutorial, +this is all you need to know: + +- We will have a directory for each tutorial step as a *project*. + +- This project will contain a ``setup.py`` which injects the features of the + project machinery into the directory. + +- In this project we will make a ``tutorial`` subdirectory into a Python + *package* using an ``__init__.py`` Python module file. + +- We will run ``pip install -e .`` to install our project in development mode. + +In summary: + +- You'll do your development in a Python *package*. + +- That package will be part of a *project*. + + +Objectives +========== + +- Make a Python "package" directory with an ``__init__.py``. + +- Get a minimum Python "project" in place by making a ``setup.py``. + +- Install our ``tutorial`` project in development mode. + + +Steps +===== + +#. Make an area for this tutorial step: + + .. code-block:: bash + + $ cd ..; mkdir package; cd package + +#. In ``package/setup.py``, enter the following: + + .. literalinclude:: package/setup.py + +#. Make the new project installed for development then make a directory for the + actual code: + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + $ mkdir tutorial + +#. Enter the following into ``package/tutorial/__init__.py``: + + .. literalinclude:: package/tutorial/__init__.py + +#. Enter the following into ``package/tutorial/app.py``: + + .. literalinclude:: package/tutorial/app.py + +#. Run the WSGI application with: + + .. code-block:: bash + + $ $VENV/bin/python tutorial/app.py + +#. Open http://localhost:6543/ in your browser. + + +Analysis +======== + +Python packages give us an organized unit of project development. Python +projects, via ``setup.py``, give us special features when our package is +installed (in this case, in local development mode, also called local editable +mode as indicated by ``-e .``). + +In this step we have a Python package called ``tutorial``. We use the same name +in each step of the tutorial, to avoid unnecessary retyping. + +Above this ``tutorial`` directory we have the files that handle the packaging +of this project. At the moment, all we need is a bare-bones ``setup.py``. + +Everything else is the same about our application. We simply made a Python +package with a ``setup.py`` and installed it in development mode. + +Note that the way we're running the app (``python tutorial/app.py``) is a bit +of an odd duck. We would never do this unless we were writing a tutorial that +tries to capture how this stuff works one step at a time. It's generally a bad +idea to run a Python module inside a package directly as a script. + +.. seealso:: :ref:`Python Packages <python:tut-packages>` and `Working in + "Development Mode" + <https://packaging.python.org/en/latest/distributing/#working-in-development-mode>`_. diff --git a/docs/quick_tutorial/package/setup.py b/docs/quick_tutorial/package/setup.py new file mode 100644 index 000000000..bcfcfa684 --- /dev/null +++ b/docs/quick_tutorial/package/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/__init__.py b/docs/quick_tutorial/package/tutorial/__init__.py new file mode 100644 index 000000000..d310fdde9 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/__init__.py @@ -0,0 +1 @@ +# package
\ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/app.py b/docs/quick_tutorial/package/tutorial/app.py new file mode 100644 index 000000000..210075023 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print ('Incoming request') + return Response('<body><h1>Hello World!</h1></body>') + + +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()
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response.rst b/docs/quick_tutorial/request_response.rst new file mode 100644 index 000000000..0ac9b4f6d --- /dev/null +++ b/docs/quick_tutorial/request_response.rst @@ -0,0 +1,109 @@ +.. _qtut_request_response: + +======================================= +10: Handling Web Requests and Responses +======================================= + +Web applications handle incoming requests and return outgoing responses. +Pyramid makes working with requests and responses convenient and reliable. + + +Objectives +========== + +- Learn the background on Pyramid's choices for requests and responses. + +- Grab data out of the request. + +- Change information in the response headers. + + +Background +========== + +Developing for the web means processing web requests. As this is a critical +part of a web application, web developers need a robust, mature set of software +for web requests and returning web responses. + +Pyramid has always fit nicely into the existing world of Python web development +(virtual environments, packaging, scaffolding, first to embrace Python 3, and +so on). Pyramid turned to the well-regarded :term:`WebOb` Python library for +request and response handling. In our example above, Pyramid hands +``hello_world`` a ``request`` that is :ref:`based on WebOb <webob_chapter>`. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes request_response; cd request_response + $ $VENV/bin/pip install -e . + +#. Simplify the routes in ``request_response/tutorial/__init__.py``: + + .. literalinclude:: request_response/tutorial/__init__.py + :linenos: + +#. We only need one view in ``request_response/tutorial/views.py``: + + .. literalinclude:: request_response/tutorial/views.py + :linenos: + +#. Update the tests in ``request_response/tutorial/tests.py``: + + .. literalinclude:: request_response/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + ..... + 5 passed in 0.30 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. You will be redirected to + http://localhost:6543/plain. + +#. Open http://localhost:6543/plain?name=alice in your browser. + + +Analysis +======== + +In this view class, we have two routes and two views, with the first leading to +the second by an HTTP redirect. Pyramid can :ref:`generate redirects +<http_redirect>` by returning a special object from a view or raising a special +exception. + +In this Pyramid view, we get the URL being visited from ``request.url``. Also, +if you visited http://localhost:6543/plain?name=alice, the name is included in +the body of the response: + +.. code-block:: text + + URL http://localhost:6543/plain?name=alice with name: alice + +Finally, we set the response's content type and body, then return the response. + +We updated the unit and functional tests to prove that our code does the +redirection, but also handles sending and not sending ``/plain?name``. + + +Extra credit +============ + +#. Could we also ``raise HTTPFound(location='/plain')`` instead of returning + it? If so, what's the difference? + +.. seealso:: :ref:`webob_chapter`, + :ref:`generate redirects <http_redirect>` diff --git a/docs/quick_tutorial/request_response/development.ini b/docs/quick_tutorial/request_response/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/request_response/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/request_response/setup.py b/docs/quick_tutorial/request_response/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/request_response/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/__init__.py b/docs/quick_tutorial/request_response/tutorial/__init__.py new file mode 100644 index 000000000..77a172888 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('plain', '/plain') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/tests.py b/docs/quick_tutorial/request_response/tutorial/tests.py new file mode 100644 index 000000000..7486c2b2d --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/tests.py @@ -0,0 +1,54 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual(response.status, '302 Found') + + def test_plain_without_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.plain() + self.assertIn(b'No Name Provided', response.body) + + def test_plain_with_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + request.GET['name'] = 'Jane Doe' + inst = TutorialViews(request) + response = inst.plain() + self.assertIn(b'Jane Doe', response.body) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_plain_without_name(self): + res = self.testapp.get('/plain', status=200) + self.assertIn(b'No Name Provided', res.body) + + def test_plain_with_name(self): + res = self.testapp.get('/plain?name=Jane%20Doe', status=200) + self.assertIn(b'Jane Doe', res.body) diff --git a/docs/quick_tutorial/request_response/tutorial/views.py b/docs/quick_tutorial/request_response/tutorial/views.py new file mode 100644 index 000000000..8c7ff5f37 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/views.py @@ -0,0 +1,22 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.view import view_config + + +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return HTTPFound(location='/plain') + + @view_config(route_name='plain') + def plain(self): + name = self.request.params.get('name', 'No Name Provided') + + body = 'URL %s with name: %s' % (self.request.url, name) + return Response( + content_type='text/plain', + body=body + ) diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst new file mode 100644 index 000000000..1f2b4da97 --- /dev/null +++ b/docs/quick_tutorial/requirements.rst @@ -0,0 +1,214 @@ +.. _qtut_requirements: + +============ +Requirements +============ + +Let's get our tutorial environment set up. Most of the set up work is in +standard Python development practices (install Python and make an isolated +virtual environment.) + +.. note:: + + Pyramid encourages standard Python development practices with packaging + tools, virtual environments, logging, and so on. There are many variations, + implementations, and opinions across the Python community. For consistency, + ease of documentation maintenance, and to minimize confusion, the Pyramid + *documentation* has adopted specific conventions that are consistent with the + :term:`Python Packaging Authority`. + +This *Quick Tutorial* is based on: + +* **Python 3.5**. Pyramid fully supports Python 3.3+ and Python 2.7+. This + tutorial uses **Python 3.5** but runs fine under Python 2.7. + +* **venv**. We believe in virtual environments. For this tutorial, we use + Python 3.5's built-in solution :term:`venv`. For Python 2.7, you can install + :term:`virtualenv`. + +* **pip**. We use :term:`pip` for package management. + +* **Workspaces, projects, and packages.** Our home directory will contain a + *tutorial workspace* with our Python virtual environment and *Python + projects* (a directory with packaging information and *Python packages* of + working code.) + +* **Unix commands**. Commands in this tutorial use UNIX syntax and paths. + Windows users should adjust commands accordingly. + +.. note:: + Pyramid was one of the first web frameworks to fully support Python 3 in + October 2011. + +.. note:: + Windows commands use the plain old MSDOS shell. For PowerShell command + syntax, see its documentation. + +Steps +===== + +#. :ref:`install-python-3` +#. :ref:`create-a-project-directory-structure` +#. :ref:`set-an-environment-variable` +#. :ref:`create-a-virtual-environment` +#. :ref:`install-pyramid` + + +.. _install-python-3: + +Install Python 3 +---------------- + +See the detailed recommendation for your operating system described under +:ref:`installing_chapter`. + +- :ref:`for-mac-os-x-users` +- :ref:`if-you-don-t-yet-have-a-python-interpreter-unix` +- :ref:`if-you-don-t-yet-have-a-python-interpreter-windows` + + +.. _create-a-project-directory-structure: + +Create a project directory structure +------------------------------------ + +We will arrive at a directory structure of ``workspace -> project -> package``, +where our workspace is named ``quick_tutorial``. The following tree diagram +shows how this will be structured, and where our :term:`virtual environment` +will reside as we proceed through the tutorial: + +.. code-block:: text + + └── ~ + └── projects + └── quick_tutorial + ├── env + └── step_one + ├── intro + │ ├── __init__.py + │ └── app.py + └── setup.py + +For Linux, the commands to do so are as follows: + +.. code-block:: bash + + # Mac and Linux + $ cd ~ + $ mkdir -p projects/quick_tutorial + $ cd projects/quick_tutorial + +For Windows: + +.. code-block:: doscon + + # Windows + c:\> cd \ + c:\> mkdir projects\quick_tutorial + c:\> cd projects\quick_tutorial + +In the above figure, your user home directory is represented by ``~``. In your +home directory, all of your projects are in the ``projects`` directory. This is +a general convention not specific to Pyramid that many developers use. Windows +users will do well to use ``c:\`` as the location for ``projects`` in order to +avoid spaces in any of the path names. + +Next within ``projects`` is your workspace directory, here named +``quick_tutorial``. A workspace is a common term used by integrated +development environments (IDE), like PyCharm and PyDev, where virtual +environments, specific project files, and repositories are stored. + + +.. _set-an-environment-variable: + +Set an environment variable +--------------------------- + +This tutorial will refer frequently to the location of the :term:`virtual +environment`. We set an environment variable to save typing later. + +.. code-block:: bash + + # Mac and Linux + $ export VENV=~/projects/quick_tutorial/env + +.. code-block:: doscon + + # Windows + c:\> set VENV=c:\projects\quick_tutorial\env + + +.. _create-a-virtual-environment: + +Create a virtual environment +---------------------------- + +``venv`` is a tool to create isolated Python 3 environments, each with its own +Python binary and independent set of installed Python packages in its site +directories. Let's create one, using the location we just specified in the +environment variable. + +.. code-block:: bash + + # Mac and Linux + $ python3 -m venv $VENV + +.. code-block:: doscon + + # Windows + c:\> c:\Python35\python3 -m venv %VENV% + +.. seealso:: See also Python 3's :mod:`venv module <python:venv>` and Python + 2's `virtualenv <https://virtualenv.pypa.io/en/latest/>`_ package. + + +Update packaging tools in the virtual environment +------------------------------------------------- + +It's always a good idea to update to the very latest version of packaging tools +because the installed Python bundles only the version that was available at the +time of its release. + +.. code-block:: bash + + # Mac and Linux + $VENV/bin/pip install --upgrade pip setuptools + +.. code-block:: doscon + + # Windows + c:\> %VENV%\Scripts\pip install --upgrade pip setuptools + + +.. _install-pyramid: + +Install Pyramid +--------------- + +We have our Python standard prerequisites out of the way. The Pyramid +part is pretty easy. + +.. parsed-literal:: + + # Mac and Linux + $ $VENV/bin/pip install "pyramid==\ |release|\ " + + # Windows + c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ " + +Our Python virtual environment now has the Pyramid software available. + +You can optionally install some of the extra Python packages used in this +tutorial. + +.. code-block:: bash + + # Mac and Linux + $ $VENV/bin/pip install webtest pytest pytest-cov deform sqlalchemy \ + pyramid_chameleon pyramid_debugtoolbar pyramid_jinja2 waitress \ + pyramid_tm zope.sqlalchemy + +.. code-block:: doscon + + # Windows + c:\> %VENV%\Scripts\pip install webtest deform sqlalchemy pyramid_chameleon pyramid_debugtoolbar pyramid_jinja2 waitress pyramid_tm zope.sqlalchemy diff --git a/docs/quick_tutorial/retail_forms/development.ini b/docs/quick_tutorial/retail_forms/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/retail_forms/setup.py b/docs/quick_tutorial/retail_forms/setup.py new file mode 100644 index 000000000..361ade013 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/__init__.py b/docs/quick_tutorial/retail_forms/tutorial/__init__.py new file mode 100644 index 000000000..dff7457cf --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/retail_forms/tutorial/tests.py b/docs/quick_tutorial/retail_forms/tutorial/tests.py new file mode 100644 index 000000000..5a2c40904 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(len(response['pages']), 3) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<title>Wiki: View</title>', res.body) diff --git a/docs/quick_tutorial/retail_forms/tutorial/views.py b/docs/quick_tutorial/retail_forms/tutorial/views.py new file mode 100644 index 000000000..2737ebdc4 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +pages = { + '100': dict(uid='100', title='Page 100', body='<em>100</em>'), + '101': dict(uid='101', title='Page 101', body='<em>101</em>'), + '102': dict(uid='102', title='Page 102', body='<em>102</em>') +} + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + return dict(pages=pages.values()) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Form is valid, make a new identifier and add to list + last_uid = int(sorted(pages.keys())[-1]) + new_uid = str(last_uid + 1) + pages[new_uid] = dict( + uid=new_uid, title=appstruct['title'], + body=appstruct['body'] + ) + + # Now visit new page + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + return dict(page=page) + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + + url = self.request.route_url('wikipage_view', + uid=page['uid']) + return HTTPFound(url) + + form = wiki_form.render(page) + + return dict(page=page, form=form)
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt b/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..586f4c44b --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<div class="row" + tal:repeat="field form"> + <div class="span2"> + ${structure:field.title} + <span class="req" tal:condition="field.required">*</span> + </div> + <div class="span2"> + ${structure:field.serialize()} + </div> + <ul tal:condition="field.error"> + <li tal:repeat="error field.error.messages()"> + ${structure:error} + </li> + </ul> +</div> + +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt b/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst new file mode 100644 index 000000000..27c8c2c22 --- /dev/null +++ b/docs/quick_tutorial/routing.rst @@ -0,0 +1,124 @@ +.. _qtut_routing: + +========================================== +11: Dispatching URLs To Views With Routing +========================================== + +Routing matches incoming URL patterns to view code. Pyramid's routing has a +number of useful features. + + +Background +========== + +Writing web applications usually means sophisticated URL design. We just saw +some Pyramid machinery for requests and views. Let's look at features that help +in routing. + +Previously we saw the basics of routing URLs to views in Pyramid. + +- Your project's "setup" code registers a route name to be used when matching + part of the URL + +- Elsewhere a view is configured to be called for that route name. + +.. note:: + + Why do this twice? Other Python web frameworks let you create a route and + associate it with a view in one step. As illustrated in + :ref:`routes_need_ordering`, multiple routes might match the same URL + pattern. Rather than provide ways to help guess, Pyramid lets you be + explicit in ordering. Pyramid also gives facilities to avoid the problem. + It's relatively easy to build a system that uses implicit route ordering + with Pyramid too. See `The Groundhog series of screencasts + <http://static.repoze.org/casts/videotags.html>`_ if you're interested in + doing so. + + +Objectives +========== + +- Define a route that extracts part of the URL into a Python dictionary. + +- Use that dictionary data in a view. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes routing; cd routing + $ $VENV/bin/pip install -e . + +#. Our ``routing/tutorial/__init__.py`` needs a route with a replacement + pattern: + + .. literalinclude:: routing/tutorial/__init__.py + :linenos: + +#. We just need one view in ``routing/tutorial/views.py``: + + .. literalinclude:: routing/tutorial/views.py + :linenos: + +#. We just need one view in ``routing/tutorial/home.pt``: + + .. literalinclude:: routing/tutorial/home.pt + :language: html + :linenos: + +#. Update ``routing/tutorial/tests.py``: + + .. literalinclude:: routing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q + .. + 2 passed in 0.39 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy/amy/smith in your browser. + + +Analysis +======== + +In ``__init__.py`` we see an important change in our route declaration: + +.. code-block:: python + + config.add_route('hello', '/howdy/{first}/{last}') + +With this we tell the :term:`configurator` that our URL has a "replacement +pattern". 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: + +.. code-block:: python + + self.request.matchdict['first'] + self.request.matchdict['last'] + +``request.matchdict`` contains values from the URL that match the "replacement +patterns" (the curly braces) in the route declaration. This information can +then be used anywhere in Pyramid that has access to the request. + +Extra credit +============ + +#. What happens if you to go the URL http://localhost:6543/howdy? Is this the + result that you expected? + +.. seealso:: `Weird Stuff You Can Do With URL Dispatch + <http://www.plope.com/weird_pyramid_urldispatch>`_ diff --git a/docs/quick_tutorial/routing/development.ini b/docs/quick_tutorial/routing/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/routing/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/routing/setup.py b/docs/quick_tutorial/routing/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/routing/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/routing/tutorial/__init__.py b/docs/quick_tutorial/routing/tutorial/__init__.py new file mode 100644 index 000000000..4b2dac36d --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/howdy/{first}/{last}') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/routing/tutorial/home.pt b/docs/quick_tutorial/routing/tutorial/home.pt new file mode 100644 index 000000000..b68e96338 --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/home.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>${name}</h1> +<p>First: ${first}, Last: ${last}</p> +</body> +</html> diff --git a/docs/quick_tutorial/routing/tutorial/tests.py b/docs/quick_tutorial/routing/tutorial/tests.py new file mode 100644 index 000000000..572f389fb --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + request.matchdict['first'] = 'First' + request.matchdict['last'] = 'Last' + inst = TutorialViews(request) + response = inst.home() + self.assertEqual(response['first'], 'First') + self.assertEqual(response['last'], 'Last') + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/howdy/Jane/Doe', status=200) + self.assertIn(b'Jane', res.body) + self.assertIn(b'Doe', res.body) diff --git a/docs/quick_tutorial/routing/tutorial/views.py b/docs/quick_tutorial/routing/tutorial/views.py new file mode 100644 index 000000000..8a9211e92 --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/views.py @@ -0,0 +1,20 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + first = self.request.matchdict['first'] + last = self.request.matchdict['last'] + return { + 'name': 'Home View', + 'first': first, + 'last': last + } diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst new file mode 100644 index 000000000..7845f2b71 --- /dev/null +++ b/docs/quick_tutorial/scaffolds.rst @@ -0,0 +1,87 @@ +.. _qtut_scaffolds: + +============================================= +Prelude: Quick Project Startup with Scaffolds +============================================= + +To ease the process of getting started, Pyramid provides *scaffolds* that +generate sample projects from templates in Pyramid and Pyramid add-ons. + + +Background +========== + +We're going to cover a lot in this tutorial, focusing on one topic at a time +and writing everything from scratch. As a warm up, though, it sure would be +nice to see some pixels on a screen. + +Like other web development frameworks, Pyramid provides a number of "scaffolds" +that generate working Python, template, and CSS code for sample applications. +In this step we'll use a built-in scaffold to let us preview a Pyramid +application, before starting from scratch on Step 1. + + +Objectives +========== + +- Use Pyramid's ``pcreate`` command to list scaffolds and make a new project. + +- Start up a Pyramid application and visit it in a web browser. + + +Steps +===== + +#. Pyramid's ``pcreate`` command can list the available scaffolds: + + .. code-block:: bash + + $ $VENV/bin/pcreate --list + Available scaffolds: + alchemy: Pyramid SQLAlchemy project using url dispatch + starter: Pyramid starter project + zodb: Pyramid ZODB project using traversal + +#. Tell ``pcreate`` to use the ``starter`` scaffold to make our project: + + .. code-block:: bash + + $ $VENV/bin/pcreate --scaffold starter scaffolds + +#. Install our project in editable mode for development in the current + directory: + + .. code-block:: bash + + $ cd scaffolds + $ $VENV/bin/pip install -e . + +#. Start up the application by pointing Pyramid's ``pserve`` command at the + project's (generated) configuration file: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + + On start up, ``pserve`` logs some output: + + .. code-block:: bash + + Starting subprocess with file monitor + Starting server in PID 72213. + Starting HTTP server on http://0.0.0.0:6543 + +#. Open http://localhost:6543/ in your browser. + +Analysis +======== + +Rather than starting from scratch, ``pcreate`` can make getting a Python +project containing a Pyramid application a quick matter. Pyramid ships with a +few scaffolds. But installing a Pyramid add-on can give you new scaffolds from +that add-on. + +``pserve`` is Pyramid's application runner, separating operational details from +your code. When you install Pyramid, a small command program called ``pserve`` +is written to your ``bin`` directory. This program is an executable Python +module. It is passed a configuration file (in this case, ``development.ini``). diff --git a/docs/quick_tutorial/scaffolds/CHANGES.txt b/docs/quick_tutorial/scaffolds/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/quick_tutorial/scaffolds/MANIFEST.in b/docs/quick_tutorial/scaffolds/MANIFEST.in new file mode 100644 index 000000000..91d3f763b --- /dev/null +++ b/docs/quick_tutorial/scaffolds/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include scaffolds *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/quick_tutorial/scaffolds/README.txt b/docs/quick_tutorial/scaffolds/README.txt new file mode 100644 index 000000000..7776dd2d5 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/README.txt @@ -0,0 +1 @@ +scaffolds README diff --git a/docs/quick_tutorial/scaffolds/development.ini b/docs/quick_tutorial/scaffolds/development.ini new file mode 100644 index 000000000..b31d06194 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/development.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:scaffolds + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, scaffolds + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_scaffolds] +level = DEBUG +handlers = +qualname = scaffolds + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/quick_tutorial/scaffolds/production.ini b/docs/quick_tutorial/scaffolds/production.ini new file mode 100644 index 000000000..1418e6bf6 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/production.ini @@ -0,0 +1,54 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:scaffolds + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, scaffolds + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_scaffolds] +level = WARN +handlers = +qualname = scaffolds + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/quick_tutorial/scaffolds/scaffolds/__init__.py b/docs/quick_tutorial/scaffolds/scaffolds/__init__.py new file mode 100644 index 000000000..ad5ecbc6f --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/favicon.ico b/docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico Binary files differindex 71f837c9e..71f837c9e 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/favicon.ico +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico diff --git a/docs/narr/MyProject/myproject/static/footerbg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png Binary files differindex 1fbc873da..1fbc873da 100644 --- a/docs/narr/MyProject/myproject/static/footerbg.png +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png diff --git a/docs/narr/MyProject/myproject/static/headerbg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png Binary files differindex 0596f2020..0596f2020 100644 --- a/docs/narr/MyProject/myproject/static/headerbg.png +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png diff --git a/docs/narr/MyProject/myproject/static/ie6.css b/docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css index b7c8493d8..b7c8493d8 100644 --- a/docs/narr/MyProject/myproject/static/ie6.css +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css diff --git a/docs/narr/MyProject/myproject/static/middlebg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png Binary files differindex 2369cfb7d..2369cfb7d 100644 --- a/docs/narr/MyProject/myproject/static/middlebg.png +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css b/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css new file mode 100644 index 000000000..4b1c017cd --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css @@ -0,0 +1,372 @@ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td +{ + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; /* 16px */ + vertical-align: baseline; + background: transparent; +} + +body +{ + line-height: 1; +} + +ol, ul +{ + list-style: none; +} + +blockquote, q +{ + quotes: none; +} + +blockquote:before, blockquote:after, q:before, q:after +{ + content: ''; + content: none; +} + +:focus +{ + outline: 0; +} + +ins +{ + text-decoration: none; +} + +del +{ + text-decoration: line-through; +} + +table +{ + border-collapse: collapse; + border-spacing: 0; +} + +sub +{ + vertical-align: sub; + font-size: smaller; + line-height: normal; +} + +sup +{ + vertical-align: super; + font-size: smaller; + line-height: normal; +} + +ul, menu, dir +{ + display: block; + list-style-type: disc; + margin: 1em 0; + padding-left: 40px; +} + +ol +{ + display: block; + list-style-type: decimal-leading-zero; + margin: 1em 0; + padding-left: 40px; +} + +li +{ + display: list-item; +} + +ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl +{ + margin-top: 0; + margin-bottom: 0; +} + +ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir +{ + list-style-type: circle; +} + +ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir +{ + list-style-type: square; +} + +.hidden +{ + display: none; +} + +p +{ + line-height: 1.5em; +} + +h1 +{ + font-size: 1.75em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h2 +{ + font-size: 1.5em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h3 +{ + font-size: 1.25em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h4 +{ + font-size: 1em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +html, body +{ + width: 100%; + height: 100%; +} + +body +{ + margin: 0; + padding: 0; + background-color: #fff; + position: relative; + font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; +} + +a +{ + color: #1b61d6; + text-decoration: none; +} + +a:hover +{ + color: #e88f00; + text-decoration: underline; +} + +body h1, body h2, body h3, body h4, body h5, body h6 +{ + font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; + font-weight: 400; + color: #373839; + font-style: normal; +} + +#wrap +{ + min-height: 100%; +} + +#header, #footer +{ + width: 100%; + color: #fff; + height: 40px; + position: absolute; + text-align: center; + line-height: 40px; + overflow: hidden; + font-size: 12px; + vertical-align: middle; +} + +#header +{ + background: #000; + top: 0; + font-size: 14px; +} + +#footer +{ + bottom: 0; + background: #000 url(footerbg.png) repeat-x 0 top; + position: relative; + margin-top: -40px; + clear: both; +} + +.header, .footer +{ + width: 750px; + margin-right: auto; + margin-left: auto; +} + +.wrapper +{ + width: 100%; +} + +#top, #top-small, #bottom +{ + width: 100%; +} + +#top +{ + color: #000; + height: 230px; + background: #fff url(headerbg.png) repeat-x 0 top; + position: relative; +} + +#top-small +{ + color: #000; + height: 60px; + background: #fff url(headerbg.png) repeat-x 0 top; + position: relative; +} + +#bottom +{ + color: #222; + background-color: #fff; +} + +.top, .top-small, .middle, .bottom +{ + width: 750px; + margin-right: auto; + margin-left: auto; +} + +.top +{ + padding-top: 40px; +} + +.top-small +{ + padding-top: 10px; +} + +#middle +{ + width: 100%; + height: 100px; + background: url(middlebg.png) repeat-x; + border-top: 2px solid #fff; + border-bottom: 2px solid #b2b2b2; +} + +.app-welcome +{ + margin-top: 25px; +} + +.app-name +{ + color: #000; + font-weight: 700; +} + +.bottom +{ + padding-top: 50px; +} + +#left +{ + width: 350px; + float: left; + padding-right: 25px; +} + +#right +{ + width: 350px; + float: right; + padding-left: 25px; +} + +.align-left +{ + text-align: left; +} + +.align-right +{ + text-align: right; +} + +.align-center +{ + text-align: center; +} + +ul.links +{ + margin: 0; + padding: 0; +} + +ul.links li +{ + list-style-type: none; + font-size: 14px; +} + +form +{ + border-style: none; +} + +fieldset +{ + border-style: none; +} + +input +{ + color: #222; + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 12px; + line-height: 16px; +} + +input[type=text], input[type=password] +{ + width: 205px; +} + +input[type=submit] +{ + background-color: #ddd; + font-weight: 700; +} + +/*Opera Fix*/ +body:before +{ + content: ""; + height: 100%; + float: left; + width: 0; + margin-top: -32767px; +} diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.png b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid-small.png Binary files differindex a5bc0ade7..a5bc0ade7 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-small.png +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid-small.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png Binary files differnew file mode 100644 index 000000000..347e05549 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png diff --git a/docs/narr/MyProject/myproject/static/transparent.gif b/docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif Binary files differindex 0341802e5..0341802e5 100644 --- a/docs/narr/MyProject/myproject/static/transparent.gif +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt index d98420680..b43a174e3 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt +++ b/docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt @@ -1,29 +1,30 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> <head> - <title>The Pyramid Web Application Development Framework</title> + <title>The Pyramid Web Framework</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> + <link rel="shortcut icon" href="${request.static_url('scaffolds:static/favicon.ico')}" /> + <link rel="stylesheet" href="${request.static_url('scaffolds:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="${request.static_url('scaffolds:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> <![endif]--> </head> <body> <div id="wrap"> <div id="top"> <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + <div><img src="${request.static_url('scaffolds:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> </div> </div> <div id="middle"> <div class="middle align-center"> <p class="app-welcome"> Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. + the Pyramid Web Framework. </p> </div> </div> @@ -31,7 +32,7 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/search.html"> <input type="text" id="q" name="q" value="" /> <input type="submit" id="x" value="Go" /> </form> @@ -43,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> @@ -68,8 +69,5 @@ </div> </div> </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> </body> </html> diff --git a/docs/quick_tutorial/scaffolds/scaffolds/tests.py b/docs/quick_tutorial/scaffolds/scaffolds/tests.py new file mode 100644 index 000000000..4f906ffa9 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/tests.py @@ -0,0 +1,17 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], 'scaffolds') diff --git a/docs/quick_tutorial/scaffolds/scaffolds/views.py b/docs/quick_tutorial/scaffolds/scaffolds/views.py new file mode 100644 index 000000000..db90d8364 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/views.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='home', renderer='templates/mytemplate.pt') +def my_view(request): + return {'project': 'scaffolds'} diff --git a/docs/quick_tutorial/scaffolds/setup.py b/docs/quick_tutorial/scaffolds/setup.py new file mode 100644 index 000000000..ec95946a5 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/setup.py @@ -0,0 +1,42 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'waitress', + ] + +setup(name='scaffolds', + version='0.0', + description='scaffolds', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require=requires, + test_suite="scaffolds", + entry_points="""\ + [paste.app_factory] + main = scaffolds:main + """, + ) diff --git a/docs/quick_tutorial/sessions.rst b/docs/quick_tutorial/sessions.rst new file mode 100644 index 000000000..df4887a4b --- /dev/null +++ b/docs/quick_tutorial/sessions.rst @@ -0,0 +1,104 @@ +.. _qtut_sessions: + +================================= +17: Transient Data Using Sessions +================================= + +Store and retrieve non-permanent data in Pyramid sessions. + + +Background +========== + +When people use your web application, they frequently perform a task that +requires semi-permanent data to be saved. For example, a shopping cart. This is +called a :term:`session`. + +Pyramid has basic built-in support for sessions. Third party packages such as +`pyramid_redis_sessions +<https://github.com/ericrasmussen/pyramid_redis_sessions>`_ provide richer +session support. Or you can create your own custom sessioning engine. Let's +take a look at the :doc:`built-in sessioning support <../narr/sessions>`. + + +Objectives +========== + +- Make a session factory using a built-in, simple Pyramid sessioning system. + +- Change our code to use a session. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes sessions; cd sessions + $ $VENV/bin/pip install -e . + +#. Our ``sessions/tutorial/__init__.py`` needs a choice of session factory to + get registered with the :term:`configurator`: + + .. literalinclude:: sessions/tutorial/__init__.py + :linenos: + +#. Our views in ``sessions/tutorial/views.py`` can now use ``request.session``: + + .. literalinclude:: sessions/tutorial/views.py + :linenos: + +#. The template at ``sessions/tutorial/home.pt`` can display the value: + + .. literalinclude:: sessions/tutorial/home.pt + :language: html + :linenos: + +#. Make sure the tests still pass: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.42 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser. + As you reload and switch between those URLs, note that the counter increases + and is *not* specific to the URL. + +#. Restart the application and revisit the page. Note that counter still + increases from where it left off. + + +Analysis +======== + +Pyramid's :term:`request` object now has a ``session`` attribute that we can +use in our view code. It acts like a dictionary. + +Since all the views are using the same counter, we made the counter a Python +property at the view class level. With this, each reload will increase the +counter displayed in our template. + +In web development, "flash messages" are notes for the user that need to appear +on a screen after a future web request. For example, when you add an item using +a form ``POST``, the site usually issues a second HTTP Redirect web request to +view the new item. You might want a message to appear after that second web +request saying "Your item was added." You can't just return it in the web +response for the POST, as it will be tossed out during the second web request. + +Flash messages are a technique where messages can be stored between requests, +using sessions, then removed when they finally get displayed. + +.. seealso:: + :ref:`sessions_chapter`, + :ref:`flash_messages`, and + :ref:`session_module`. diff --git a/docs/quick_tutorial/sessions/development.ini b/docs/quick_tutorial/sessions/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/sessions/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/sessions/setup.py b/docs/quick_tutorial/sessions/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/sessions/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/sessions/tutorial/__init__.py b/docs/quick_tutorial/sessions/tutorial/__init__.py new file mode 100644 index 000000000..9ddc2e1b1 --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/__init__.py @@ -0,0 +1,14 @@ +from pyramid.config import Configurator +from pyramid.session import SignedCookieSessionFactory + + +def main(global_config, **settings): + my_session_factory = SignedCookieSessionFactory( + 'itsaseekreet') + config = Configurator(settings=settings, + session_factory=my_session_factory) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/sessions/tutorial/home.pt b/docs/quick_tutorial/sessions/tutorial/home.pt new file mode 100644 index 000000000..50342e52f --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/home.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +<p>Count: ${view.counter}</p> +</body> +</html> diff --git a/docs/quick_tutorial/sessions/tutorial/tests.py b/docs/quick_tutorial/sessions/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/sessions/tutorial/views.py b/docs/quick_tutorial/sessions/tutorial/views.py new file mode 100644 index 000000000..a4659d265 --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/views.py @@ -0,0 +1,29 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @property + def counter(self): + session = self.request.session + if 'counter' in session: + session['counter'] += 1 + else: + session['counter'] = 1 + + return session['counter'] + + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/static_assets.rst b/docs/quick_tutorial/static_assets.rst new file mode 100644 index 000000000..65b34f8f9 --- /dev/null +++ b/docs/quick_tutorial/static_assets.rst @@ -0,0 +1,94 @@ +.. _qtut_static_assets: + +========================================== +13: CSS/JS/Images Files With 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 where Pyramid will serve +some static assets. + +Objectives +========== + +- Publish a directory of static assets at a URL. + +- Use Pyramid to help generate URLs to files in that directory. + + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes static_assets; cd static_assets + $ $VENV/bin/pip install -e . + +#. We add a call ``config.add_static_view`` in + ``static_assets/tutorial/__init__.py``: + + .. literalinclude:: static_assets/tutorial/__init__.py + :linenos: + +#. We can add a CSS link in the ``<head>`` of our template at + ``static_assets/tutorial/home.pt``: + + .. literalinclude:: static_assets/tutorial/home.pt + :language: html + +#. Add a CSS file at ``static_assets/tutorial/static/app.css``: + + .. literalinclude:: static_assets/tutorial/static/app.css + :language: css + +#. Make sure we haven't broken any existing code by running the tests: + + .. code-block:: bash + + $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.50 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser and note the new font. + + +Analysis +======== + +We changed our WSGI application to map requests under +http://localhost:6543/static/ to files and directories inside a ``static`` +directory inside our ``tutorial`` package. This directory contained +``app.css``. + +We linked to the CSS in our template. We could have hard-coded this link to +``/static/app.css``. But what if the site is later moved under +``/somesite/static/``? Or perhaps the web developer changes the arrangement on +disk? Pyramid gives a helper that provides flexibility on URL generation: + +.. code-block:: html + + ${request.static_url('tutorial:static/app.css')} + +This matches the ``path='tutorial:static'`` in our ``config.add_static_view`` +registration. 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. + + +Extra credit +============ + +#. There is also a ``request.static_path`` API. How does this differ from + ``request.static_url``? + +.. seealso:: :ref:`assets_chapter`, + :ref:`preventing_http_caching`, and + :ref:`influencing_http_caching` diff --git a/docs/quick_tutorial/static_assets/development.ini b/docs/quick_tutorial/static_assets/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/static_assets/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/static_assets/setup.py b/docs/quick_tutorial/static_assets/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/static_assets/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/__init__.py b/docs/quick_tutorial/static_assets/tutorial/__init__.py new file mode 100644 index 000000000..e244c2997 --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/__init__.py @@ -0,0 +1,11 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_static_view(name='static', path='tutorial:static') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/home.pt b/docs/quick_tutorial/static_assets/tutorial/home.pt new file mode 100644 index 000000000..57867a1ff --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/home.pt @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> + <link rel="stylesheet" + href="${request.static_url('tutorial:static/app.css') }"/> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/static_assets/tutorial/static/app.css b/docs/quick_tutorial/static_assets/tutorial/static/app.css new file mode 100644 index 000000000..f8acf3164 --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/static/app.css @@ -0,0 +1,4 @@ +body { + margin: 2em; + font-family: sans-serif; +}
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/tests.py b/docs/quick_tutorial/static_assets/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/static_assets/tutorial/views.py b/docs/quick_tutorial/static_assets/tutorial/views.py new file mode 100644 index 000000000..a56c0adbf --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/views.py @@ -0,0 +1,18 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/templating.rst b/docs/quick_tutorial/templating.rst new file mode 100644 index 000000000..ec6de98f8 --- /dev/null +++ b/docs/quick_tutorial/templating.rst @@ -0,0 +1,120 @@ +.. _qtut_templating: + +=================================== +08: HTML Generation With Templating +=================================== + +Most web frameworks don't embed HTML in programming code. Instead, they pass +data into a templating system. In this step we look at the basics of using HTML +templates in Pyramid. + + +Background +========== + +Ouch. We have been making our own ``Response`` and filling the response body +with HTML. You usually won't embed an HTML string directly in Python, but +instead will use a templating language. + +Pyramid doesn't mandate a particular database system, form library, and so on. +It encourages replaceability. This applies equally to templating, which is +fortunate: developers have strong views about template languages. As of +Pyramid 1.5a2, Pyramid doesn't even bundle a template language! + +It does, however, have strong ties to Jinja2, Mako, and Chameleon. In this step +we see how to add `pyramid_chameleon +<https://github.com/Pylons/pyramid_chameleon>`_ to your project, then change +your views to use templating. + + +Objectives +========== + +- Enable the ``pyramid_chameleon`` Pyramid add-on. + +- Generate HTML from template files. + +- Connect the templates as "renderers" for view code. + +- Change the view code to simply return data. + + +Steps +===== + +#. Let's begin by using the previous package as a starting point for a new + project: + + .. code-block:: bash + + $ cd ..; cp -r views templating; cd templating + +#. This step depends on ``pyramid_chameleon``, so add it as a dependency in + ``templating/setup.py``: + + .. literalinclude:: templating/setup.py + :linenos: + +#. Now we can activate the development-mode distribution: + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + +#. We need to connect ``pyramid_chameleon`` as a renderer by making a call in + the setup of ``templating/tutorial/__init__.py``: + + .. literalinclude:: templating/tutorial/__init__.py + :linenos: + +#. Our ``templating/tutorial/views.py`` no longer has HTML in it: + + .. literalinclude:: templating/tutorial/views.py + :linenos: + +#. Instead we have ``templating/tutorial/home.pt`` as a template: + + .. literalinclude:: templating/tutorial/home.pt + :language: html + +#. For convenience, change ``templating/development.ini`` to reload templates + automatically with ``pyramid.reload_templates``: + + .. literalinclude:: templating/development.ini + :language: ini + +#. Our unit tests in ``templating/tutorial/tests.py`` can focus on data: + + .. literalinclude:: templating/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.46 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser. + + +Analysis +======== + +Ahh, that looks better. We have a view that is focused on Python code. Our +``@view_config`` decorator specifies a :term:`renderer` that points to our +template file. Our view then simply returns data which is then supplied to our +template. Note that we used the same template for both views. + +Note the effect on testing. We can focus on having a data-oriented contract +with our view code. + +.. seealso:: :ref:`templates_chapter`, :ref:`debugging_templates`, and + :ref:`available_template_system_bindings`. diff --git a/docs/quick_tutorial/templating/development.ini b/docs/quick_tutorial/templating/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/templating/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/templating/setup.py b/docs/quick_tutorial/templating/setup.py new file mode 100644 index 000000000..0b71b73e6 --- /dev/null +++ b/docs/quick_tutorial/templating/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +) diff --git a/docs/quick_tutorial/templating/tutorial/__init__.py b/docs/quick_tutorial/templating/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/templating/tutorial/home.pt b/docs/quick_tutorial/templating/tutorial/home.pt new file mode 100644 index 000000000..fd4ef8764 --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tutorial: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html> diff --git a/docs/quick_tutorial/templating/tutorial/tests.py b/docs/quick_tutorial/templating/tutorial/tests.py new file mode 100644 index 000000000..d06a62982 --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import home + + request = testing.DummyRequest() + response = home(request) + # Our view now returns data + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import hello + + request = testing.DummyRequest() + response = hello(request) + # Our view now returns data + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/templating/tutorial/views.py b/docs/quick_tutorial/templating/tutorial/views.py new file mode 100644 index 000000000..979d69c43 --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/views.py @@ -0,0 +1,13 @@ +from pyramid.view import view_config + + +# First view, available at http://localhost:6543/ +@view_config(route_name='home', renderer='home.pt') +def home(request): + return {'name': 'Home View'} + + +# /howdy +@view_config(route_name='hello', renderer='home.pt') +def hello(request): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/tutorial_approach.rst b/docs/quick_tutorial/tutorial_approach.rst new file mode 100644 index 000000000..6d534fe13 --- /dev/null +++ b/docs/quick_tutorial/tutorial_approach.rst @@ -0,0 +1,47 @@ +================= +Tutorial Approach +================= + +This tutorial uses conventions to keep the introduction focused and concise. +Details, references, and deeper discussions are mentioned in "See also" notes. + +.. seealso:: This is an example "See also" note. + +This "Getting Started" tutorial is broken into independent steps, starting with +the smallest possible "single file WSGI app" example. Each of these steps +introduce a topic and a very small set of concepts via working code. The steps +each correspond to a directory in this repo, where each step/topic/directory is +a Python package. + +To successfully run each step: + +.. code-block:: bash + + $ cd request_response + $ $VENV/bin/pip install -e . + +...and repeat for each step you would like to work on. In most cases we will +start with the results of an earlier step. + +Directory tree +============== + +As we develop our tutorial, our directory tree will resemble the structure +below: + +.. code-block:: text + + quick_tutorial + ├── env + └── request_response + ├── tutorial + │ ├── __init__.py + │ ├── tests.py + │ └── views.py + ├── development.ini + └── setup.py + +Each of the first-level directories (e.g., ``request_response``) is a *Python +project* (except as noted for the ``hello_world`` step). The ``tutorial`` +directory is a *Python package*. At the end of each step, we copy a previous +directory into a new directory to use as a starting point. diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst new file mode 100644 index 000000000..56fd2b297 --- /dev/null +++ b/docs/quick_tutorial/unit_testing.rst @@ -0,0 +1,117 @@ +.. _qtut_unit_testing: + +============================= +05: Unit Tests and ``pytest`` +============================= + +Provide unit testing for our project's Python code. + + +Background +========== + +As the mantra says, "Untested code is broken code." The Python community has +had a long culture of writing test scripts which ensure that your code works +correctly as you write it and maintain it in the future. Pyramid has always had +a deep commitment to testing, with 100% test coverage from the earliest +pre-releases. + +Python includes a :ref:`unit testing framework +<python:unittest-minimal-example>` in its standard library. Over the years a +number of Python projects, such as :ref:`pytest <pytest:features>`, have +extended this framework with alternative test runners that provide more +convenience and functionality. The Pyramid developers use ``pytest``, which +we'll use in this tutorial. + +Don't worry, this tutorial won't be pedantic about "test-driven development" +(TDD). We'll do just enough to ensure that, in each step, we haven't majorly +broken the code. As you're writing your code, you might find this more +convenient than changing to your browser constantly and clicking reload. + +We'll also leave discussion of `pytest-cov +<http://pytest-cov.readthedocs.org/en/latest/>`_ for another section. + + +Objectives +========== + +- Write unit tests that ensure the quality of our code. + +- Install a Python package (``pytest``) which helps in our testing. + + +Steps +===== + +#. First we copy the results of the previous step, as well as install the + ``pytest`` package: + + .. code-block:: bash + + $ cd ..; cp -r debugtoolbar unit_testing; cd unit_testing + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install pytest + +#. Now we write a simple unit test in ``unit_testing/tutorial/tests.py``: + + .. literalinclude:: unit_testing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/py.test tutorial/tests.py -q + . + 1 passed in 0.14 seconds + + +Analysis +======== + +Our ``tests.py`` imports the Python standard unit testing framework. To make +writing Pyramid-oriented tests more convenient, Pyramid supplies some +``pyramid.testing`` helpers which we use in the test setup and teardown. Our +one test imports the view, makes a dummy request, and sees if the view returns +what we expect. + +The ``tests.TutorialViewTests.test_hello_world`` test is a small example of a +unit test. First, we import the view inside each test. Why not import at the +top, like in normal Python code? Because imports can cause effects that break a +test. We'd like our tests to be in *units*, hence the name *unit* testing. Each +test should isolate itself to the correct degree. + +Our test then makes a fake incoming web request, then calls our Pyramid view. +We test the HTTP status code on the response to make sure it matches our +expectations. + +Note that our use of ``pyramid.testing.setUp()`` and +``pyramid.testing.tearDown()`` aren't actually necessary here; they are only +necessary when your test needs to make use of the ``config`` object (it's a +Configurator) to add stuff to the configuration state before calling the view. + + +Extra Credit +============ + +#. Change the test to assert that the response status code should be ``404`` + (meaning, not found). Run ``py.test`` again. Read the error report and see + if you can decipher what it is telling you. + +#. As a more realistic example, put the ``tests.py`` back as you found it, and + put an error in your view, such as a reference to a non-existing variable. + Run the tests and see how this is more convenient than reloading your + browser and going back to your code. + +#. Finally, for the most realistic test, read about Pyramid ``Response`` + objects and see how to change the response code. Run the tests and see how + testing confirms the "contract" that your code claims to support. + +#. How could we add a unit test assertion to test the HTML value of the + response body? + +#. Why do we import the ``hello_world`` view function *inside* the + ``test_hello_world`` method instead of at the top of the module? + +.. seealso:: See also :ref:`testing_chapter` diff --git a/docs/quick_tutorial/unit_testing/development.ini b/docs/quick_tutorial/unit_testing/development.ini new file mode 100644 index 000000000..52b2a3a41 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/development.ini @@ -0,0 +1,9 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/unit_testing/setup.py b/docs/quick_tutorial/unit_testing/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/unit_testing/tutorial/__init__.py b/docs/quick_tutorial/unit_testing/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/unit_testing/tutorial/tests.py b/docs/quick_tutorial/unit_testing/tutorial/tests.py new file mode 100644 index 000000000..66029b421 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/tutorial/tests.py @@ -0,0 +1,18 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_hello_world(self): + from tutorial import hello_world + + request = testing.DummyRequest() + response = hello_world(request) + self.assertEqual(response.status_code, 200) diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst new file mode 100644 index 000000000..05d97a9b1 --- /dev/null +++ b/docs/quick_tutorial/view_classes.rst @@ -0,0 +1,94 @@ +.. _qtut_view_classes: + +====================================== +09: Organizing Views With View Classes +====================================== + +Change our view functions to be methods on a view class, then move some +declarations to the class level. + + +Background +========== + +So far our views have been simple, free-standing functions. Many times your +views are related to one another. They may be different ways to look at or work +on the same data, or be a REST API that handles multiple operations. Grouping +these views together as a :ref:`view class <class_as_view>` makes sense: + +- Group views. + +- Centralize some repetitive defaults. + +- Share some state and helpers. + +In this step we just do the absolute minimum to convert the existing views to a +view class. In a later tutorial step, we'll examine view classes in depth. + + +Objectives +========== + +- Group related views into a view class. + +- Centralize configuration with class-level ``@view_defaults``. + + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r templating view_classes; cd view_classes + $ $VENV/bin/pip install -e . + +#. Our ``view_classes/tutorial/views.py`` now has a view class with our two + views: + + .. literalinclude:: view_classes/tutorial/views.py + :linenos: + +#. Our unit tests in ``view_classes/tutorial/tests.py`` don't run, so let's + modify them to import the view class, and make an instance before getting a + response: + + .. literalinclude:: view_classes/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.34 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy in your browser. + + +Analysis +======== + +To ease the transition to view classes, we didn't introduce any new +functionality. We simply changed the view functions to methods on a view class, +then updated the tests. + +In our ``TutorialViews`` view class, you can see that our two view classes are +logically grouped together as methods on a common class. Since the two views +shared the same template, we could move that to a ``@view_defaults`` decorator +at the class level. + +The tests needed to change. Obviously we needed to import the view class. But +you can also see the pattern in the tests of instantiating the view class with +the dummy request first, then calling the view method being tested. + +.. seealso:: :ref:`class_as_view` diff --git a/docs/quick_tutorial/view_classes/development.ini b/docs/quick_tutorial/view_classes/development.ini new file mode 100644 index 000000000..4d47e54a5 --- /dev/null +++ b/docs/quick_tutorial/view_classes/development.ini @@ -0,0 +1,10 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/view_classes/setup.py b/docs/quick_tutorial/view_classes/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/view_classes/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/__init__.py b/docs/quick_tutorial/view_classes/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/home.pt b/docs/quick_tutorial/view_classes/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/tests.py b/docs/quick_tutorial/view_classes/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/view_classes/tutorial/views.py b/docs/quick_tutorial/view_classes/tutorial/views.py new file mode 100644 index 000000000..58db53c4a --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/views.py @@ -0,0 +1,17 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/views.rst b/docs/quick_tutorial/views.rst new file mode 100644 index 000000000..edbe4b2ff --- /dev/null +++ b/docs/quick_tutorial/views.rst @@ -0,0 +1,121 @@ +.. _qtut_views: + +================================= +07: Basic Web Handling With Views +================================= + +Organize a views module with decorators and multiple views. + + +Background +========== + +For the examples so far, the ``hello_world`` function is a "view". In Pyramid, +views are the primary way to accept web requests and return responses. + +So far our examples place everything in one file: + +- The view function + +- Its registration with the configurator + +- The route to map it to a URL + +- The WSGI application launcher + +Let's move the views out to their own ``views.py`` module and change our +startup code to scan that module, looking for decorators that set up the views. +Let's also add a second view and update our tests. + + +Objectives +========== + +- Move views into a module that is scanned by the configurator. + +- Create decorators that do declarative configuration. + + +Steps +===== + +#. Let's begin by using the previous package as a starting point for a new + distribution, then making it active: + + .. code-block:: bash + + $ cd ..; cp -r functional_testing views; cd views + $ $VENV/bin/pip install -e . + +#. Our ``views/tutorial/__init__.py`` gets a lot shorter: + + .. literalinclude:: views/tutorial/__init__.py + :linenos: + +#. Let's add a module ``views/tutorial/views.py`` that is focused on + handling requests and responses: + + .. literalinclude:: views/tutorial/views.py + :linenos: + +#. Update the tests to cover the two new views: + + .. literalinclude:: views/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/py.test tutorial/tests.py -q + .... + 4 passed in 0.28 seconds + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. + + +Analysis +======== + +We added some more URLs, but we also removed the view code from the application +startup code in ``tutorial/__init__.py``. Our views, and their view +registrations (via decorators) are now in a module ``views.py``, which is +scanned via ``config.scan('.views')``. + +We have two views, each leading to the other. If you start at +http://localhost:6543/, you get a response with a link to the next view. The +``hello`` view (available at the URL ``/howdy``) has a link back to the first +view. + +This step also shows that the name appearing in the URL, the name of the +"route" that maps a URL to a view, and the name of the view, can all be +different. More on routes later. + +Earlier we saw ``config.add_view`` as one way to configure a view. This section +introduces ``@view_config``. Pyramid's configuration supports :term:`imperative +configuration`, such as the ``config.add_view`` in the previous example. You +can also use :term:`declarative configuration`, in which a Python +:term:`python:decorator` is placed on the line above the view. Both approaches +result in the same final configuration, thus usually, it is simply a matter of +taste. + + +Extra credit +============ + +#. What does the dot in ``.views`` signify? + +#. Why might ``assertIn`` be a better choice in testing the text in responses + than ``assertEqual``? + +.. seealso:: :ref:`views_chapter`, + :ref:`view_config_chapter`, and + :ref:`debugging_view_configuration` diff --git a/docs/quick_tutorial/views/development.ini b/docs/quick_tutorial/views/development.ini new file mode 100644 index 000000000..52b2a3a41 --- /dev/null +++ b/docs/quick_tutorial/views/development.ini @@ -0,0 +1,9 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 diff --git a/docs/quick_tutorial/views/setup.py b/docs/quick_tutorial/views/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/views/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/views/tutorial/__init__.py b/docs/quick_tutorial/views/tutorial/__init__.py new file mode 100644 index 000000000..013d4538f --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/views/tutorial/tests.py b/docs/quick_tutorial/views/tutorial/tests.py new file mode 100644 index 000000000..f1757757c --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import home + + request = testing.DummyRequest() + response = home(request) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Visit', response.body) + + def test_hello(self): + from .views import hello + + request = testing.DummyRequest() + response = hello(request) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Go back', response.body) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<body>Visit', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<body>Go back', res.body) diff --git a/docs/quick_tutorial/views/tutorial/views.py b/docs/quick_tutorial/views/tutorial/views.py new file mode 100644 index 000000000..6ff149d7b --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/views.py @@ -0,0 +1,14 @@ +from pyramid.response import Response +from pyramid.view import view_config + + +# First view, available at http://localhost:6543/ +@view_config(route_name='home') +def home(request): + return Response('<body>Visit <a href="/howdy">hello</a></body>') + + +# /howdy +@view_config(route_name='hello') +def hello(request): + return Response('<body>Go back <a href="/">home</a></body>') diff --git a/docs/remake b/docs/remake new file mode 100755 index 000000000..6552716c8 --- /dev/null +++ b/docs/remake @@ -0,0 +1 @@ +make clean html SPHINXBUILD=../env/bin/sphinx-build diff --git a/docs/tutorials/.gitignore b/docs/tutorials/.gitignore new file mode 100644 index 000000000..71e689620 --- /dev/null +++ b/docs/tutorials/.gitignore @@ -0,0 +1 @@ +env*/ diff --git a/docs/tutorials/bfg/index.rst b/docs/tutorials/bfg/index.rst deleted file mode 100644 index e68e63b0b..000000000 --- a/docs/tutorials/bfg/index.rst +++ /dev/null @@ -1,204 +0,0 @@ -.. index:: - single: converting a BFG app - single: bfg2pyramid - -.. _converting_a_bfg_app: - -Converting a :mod:`repoze.bfg` Application to :app:`Pyramid` -============================================================ - -Prior iterations of :app:`Pyramid` were released as a package named -:mod:`repoze.bfg`. :mod:`repoze.bfg` users are encouraged to upgrade -their deployments to :app:`Pyramid`, as, after the first final release -of :app:`Pyramid`, further feature development on :mod:`repoze.bfg` -will cease. - -Most existing :mod:`repoze.bfg` applications can be converted to a -:app:`Pyramid` application in a completely automated fashion. -However, if your application depends on packages which are not "core" -parts of :mod:`repoze.bfg` but which nonetheless have ``repoze.bfg`` -in their names (e.g. ``repoze.bfg.skins``, -``repoze.bfg.traversalwrapper``, ``repoze.bfg.jinja2``), you will need -to find an analogue for each. For example, by the time you read this, -there will be a ``pyramid_jinja2`` package, which can be used instead -of ``repoze.bfg.jinja2``. If an analogue does not seem to exist for a -``repoze.bfg`` add-on package that your application uses, please email -the `Pylons-devel <http://groups.google.com/group/pylons-devel>`_ -maillist; we'll convert the package to a :app:`Pyramid` analogue for -you. - -Here's how to convert a :mod:`repoze.bfg` application to a -:app:`Pyramid` application: - -#. Ensure that your application works under :mod:`repoze.bfg` *version - 1.3 or better*. See - `http://docs.repoze.org/bfg/1.3/narr/install.html - <http://docs.repoze.org/bfg/1.3/narr/install.html>`_ for - :mod:`repoze.bfg` 1.3 installation instructions. If your - application has an automated test suite, run it while your - application is using :mod:`repoze.bfg` 1.3+. Otherwise, test it - manually. It is only safe to proceed to the next step once your - application works under :mod:`repoze.bfg` 1.3+. - - If your application has a proper set of dependencies, and a - standard automated test suite, you might test your - :mod:`repoze.bfg` application against :mod:`repoze.bfg` 1.3 like - so: - - .. code-block:: bash - - $ bfgenv/bin/python setup.py test - - ``bfgenv`` above will be the virtualenv into which you've installed - :mod:`repoze.bfg` 1.3. - -#. Install :app:`Pyramid` into a *separate* virtualenv as per the - instructions in :ref:`installing_chapter`. The :app:`Pyramid` - virtualenv should be separate from the one you've used to install - :mod:`repoze.bfg`. A quick way to do this: - - .. code-block:: bash - - $ cd ~ - $ virtualenv --no-site-packages pyramidenv - $ cd pyramidenv - $ bin/easy_install pyramid - -#. Put a *copy* of your :mod:`repoze.bfg` application into a temporary - location (perhaps by checking a fresh copy of the application out - of a version control repository). For example: - - .. code-block:: bash - - $ cd /tmp - $ svn co http://my.server/my/bfg/application/trunk bfgapp - -#. Use the ``bfg2pyramid`` script present in the ``bin`` directory of - the :app:`Pyramid` virtualenv to convert all :mod:`repoze.bfg` - Python import statements into compatible :app:`Pyramid` import - statements. ``bfg2pyramid`` will also fix ZCML directive usages of - common :mod:`repoze.bfg` directives. You invoke ``bfg2pyramid`` by - passing it the *path* of the copy of your application. The path - passed should contain a "setup.py" file, representing your - :mod:`repoze.bfg` application's setup script. ``bfg2pyramid`` will - change the copy of the application *in place*. - - .. code-block:: bash - - $ ~/pyramidenv/bfg2pyramid /tmp/bfgapp - - ``bfg2pyramid`` will convert the following :mod:`repoze.bfg` - application aspects to :app:`Pyramid` compatible analogues: - - - Python ``import`` statements naming :mod:`repoze.bfg` APIs will - be converted to :app:`Pyramid` compatible ``import`` statements. - Every Python file beneath the top-level path will be visited and - converted recursively, except Python files which live in - directories which start with a ``.`` (dot). - - - Each ZCML file found (recursively) within the path will have the - default ``xmlns`` attribute attached to the ``configure`` tag - changed from ``http://namespaces.repoze.org/bfg`` to - ``http://pylonshq.com/pyramid``. Every ZCML file beneath the - top-level path (files ending with ``.zcml``) will be visited and - converted recursively, except ZCML files which live in - directories which start with a ``.`` (dot). - - - ZCML files which contain directives that have attributes which - name a ``repoze.bfg`` API module or attribute of an API module - (e.g. ``context="repoze.bfg.exceptions.NotFound"``) will be - converted to :app:`Pyramid` compatible ZCML attributes - (e.g. ``context="pyramid.exceptions.NotFound``). Every ZCML file - beneath the top-level path (files ending with ``.zcml``) will be - visited and converted recursively, except ZCML files which live - in directories which start with a ``.`` (dot). - -#. Edit the ``setup.py`` file of the application you've just converted - (if you've been using the example paths, this will be - ``/tmp/bfgapp/setup.py``) to depend on the ``pyramid`` distribution - instead the of ``repoze.bfg`` distribution in its - ``install_requires`` list. If you used a scaffold to - create the :mod:`repoze.bfg` application, you can do so by changing - the ``requires`` line near the top of the ``setup.py`` file. The - original may look like this: - - .. code-block:: text - - requires = ['repoze.bfg', ... other dependencies ...] - - Edit the ``setup.py`` so it has: - - .. code-block:: text - - requires = ['pyramid', ... other dependencies ...] - - All other install-requires and tests-requires dependencies save for - the one on ``repoze.bfg`` can remain the same. - -#. Convert any ``install_requires`` dependencies your application has - on other add-on packages which have ``repoze.bfg`` in their names - to :app:`Pyramid` compatible analogues (e.g. ``repoze.bfg.jinja2`` - should be replaced with ``pyramid_jinja2``). You may need to - adjust configuration options and/or imports in your - :mod:`repoze.bfg` application after replacing these add-ons. Read - the documentation of the :app:`Pyramid` add-on package for - information. - -#. *Only if you use ZCML and add-ons which use ZCML*: The default - ``xmlns`` of the ``configure`` tag in ZCML has changed. The - ``bfg2pyramid`` script effects the default namespace change (it - changes the ``configure`` tag default ``xmlns`` from - ``http://namespaces.repoze.org/bfg`` to - ``http://pylonshq.com/pyramid``). - - This means that uses of add-ons which define ZCML directives in the - ``http://namespaces.repoze.org/bfg`` namespace will begin to "fail" - (they're actually not really failing, but your ZCML assumes that - they will always be used within a ``configure`` tag which names the - ``http://namespaces.repoze.org/bfg`` namespace as its default - ``xmlns``). Symptom: when you attempt to start the application, an - error such as ``ConfigurationError: ('Unknown directive', - u'http://namespaces.repoze.org/bfg', u'workflow')`` is printed to - the console and the application fails to start. In such a case, - either add an ``xmlns="http://namespaces.repoze.org/bfg"`` - attribute to each tag which causes a failure, or define a namespace - alias in the configure tag and prefix each failing tag. For - example, change this "failing" tag instance:: - - <configure xmlns="http://pylonshq.com/pyramid"> - <failingtag attr="foo"/> - </configure> - - To this, which will begin to succeed:: - - <configure xmlns="http://pylonshq.com/pyramid" - xmlns:bfg="http://namespaces.repoze.org/bfg"> - <bfg:failingtag attr="foo"/> - </configure> - - You will also need to add the ``pyramid_zcml`` package to your - ``setup.py`` ``install_requires`` list. In Pyramid, ZCML configuration - became an optional add-on supported by the ``pyramid_zcml`` package. - -#. Retest your application using :app:`Pyramid`. This might be as - easy as: - - .. code-block:: bash - - $ cd /tmp/bfgapp - $ ~/pyramidenv/bin/python setup.py test - -#. Fix any test failures. - -#. Fix any code which generates deprecation warnings. - -#. Start using the converted version of your application. Celebrate. - -Two terminological changes have been made to Pyramid which make its -documentation and newer APIs different than those of ``repoze.bfg``. The -concept that BFG called ``model`` is called ``resource`` in Pyramid and the -concept that BFG called ``resource`` is called ``asset`` in Pyramid. Various -APIs have changed as a result (although all have backwards compatible shims). -Additionally, the environment variables that influenced server behavior which -used to be prefixed with ``BFG_`` (such as ``BFG_DEBUG_NOTFOUND``) must now -be prefixed with ``PYRAMID_``. diff --git a/docs/tutorials/gae/index.rst b/docs/tutorials/gae/index.rst deleted file mode 100644 index 9c8e8c07e..000000000 --- a/docs/tutorials/gae/index.rst +++ /dev/null @@ -1,231 +0,0 @@ -.. _appengine_tutorial: - -Running :app:`Pyramid` on Google's App Engine -================================================ - -It is possible to run a :app:`Pyramid` application on Google's `App -Engine <http://code.google.com/appengine/>`_. Content from this -tutorial was contributed by YoungKing, based on the -`"appengine-monkey" tutorial for Pylons -<http://code.google.com/p/appengine-monkey/wiki/Pylons>`_. This -tutorial is written in terms of using the command line on a UNIX -system; it should be possible to perform similar actions on a Windows -system. - -#. Download Google's `App Engine SDK - <http://code.google.com/appengine/downloads.html>`_ and install it - on your system. - -#. Use Subversion to check out the source code for - ``appengine-monkey``. - - .. code-block:: text - - $ svn co http://appengine-monkey.googlecode.com/svn/trunk/ \ - appengine-monkey - -#. Use ``appengine_homedir.py`` script in ``appengine-monkey`` to - create a :term:`virtualenv` for your application. - - .. code-block:: text - - $ export GAE_PATH=/usr/local/google_appengine - $ python2.5 /path/to/appengine-monkey/appengine-homedir.py --gae \ - $GAE_PATH pyramidapp - - Note that ``$GAE_PATH`` should be the path where you have unpacked - the App Engine SDK. (On Mac OS X at least, - ``/usr/local/google_appengine`` is indeed where the installer puts - it). - - This will set up an environment in ``pyramidapp/``, with some tools - installed in ``pyramidapp/bin``. There will also be a directory - ``pyramidapp/app/`` which is the directory you will upload to - appengine. - -#. Install :app:`Pyramid` into the virtualenv - - .. code-block:: text - - $ cd pyramidapp/ - $ bin/easy_install pyramid - - This will install :app:`Pyramid` in the environment. - -#. Create your application - - We'll use the standard way to create a :app:`Pyramid` - application, but we'll have to move some files around when we are - done. The below commands assume your current working directory is - the ``pyramidapp`` virtualenv directory you created in the third step - above: - - .. code-block:: text - - $ cd app - $ rm -rf pyramidapp - $ bin/paster create -t pyramid_starter pyramidapp - $ mv pyramidapp aside - $ mv aside/pyramidapp . - $ rm -rf aside - -#. Edit ``config.py`` - - Edit the ``APP_NAME`` and ``APP_ARGS`` settings within - ``config.py``. The ``APP_NAME`` must be ``pyramidapp:main``, and - the APP_ARGS must be ``({},)``. Any other settings in - ``config.py`` should remain the same. - - .. code-block:: python - - APP_NAME = 'pyramidapp:main' - APP_ARGS = ({},) - -#. Edit ``runner.py`` - - To prevent errors for ``import site``, add this code stanza before - ``import site`` in app/runner.py: - - .. code-block:: python - - import sys - sys.path = [path for path in sys.path if 'site-packages' not in path] - import site - - You will also need to comment out the line that starts with - ``assert sys.path`` in the file. - - .. code-block:: python - - # comment the sys.path assertion out - # assert sys.path[:len(cur_sys_path)] == cur_sys_path, ( - # "addsitedir() caused entries to be prepended to sys.path") - - For GAE development environment 1.3.0 or better, you will also need - the following somewhere near the top of the ``runner.py`` file to - fix a compatibility issue with ``appengine-monkey``: - - .. code-block:: python - - import os - os.mkdir = None - -#. Run the application. ``dev_appserver.py`` is typically installed - by the SDK in the global path but you need to be sure to run it - with Python 2.5 (or whatever version of Python your GAE SDK - expects). - - .. code-block:: text - :linenos: - - $ cd ../.. - $ python2.5 /usr/local/bin/dev_appserver.py pyramidapp/app/ - - Startup success looks something like this: - - .. code-block:: text - - [chrism@vitaminf pyramid_gae]$ python2.5 \ - /usr/local/bin/dev_appserver.py \ - pyramidapp/app/ - INFO 2009-05-03 22:23:13,887 appengine_rpc.py:157] # ... more... - Running application pyramidapp on port 8080: http://localhost:8080 - - You may need to run "Make Symlinks" from the Google App Engine - Launcher GUI application if your system doesn't already have the - ``dev_appserver.py`` script sitting around somewhere. - -#. Hack on your pyramid application, using a normal run, debug, restart - process. For tips on how to use the ``pdb`` module within Google - App Engine, `see this blog post - <http://jjinux.blogspot.com/2008/05/python-debugging-google-app-engine-apps.html>`_. - In particular, you can create a function like so and call it to - drop your console into a pdb trace: - - .. code-block:: python - :linenos: - - def set_trace(): - import pdb, sys - debugger = pdb.Pdb(stdin=sys.__stdin__, - stdout=sys.__stdout__) - debugger.set_trace(sys._getframe().f_back) - -#. `Sign up for a GAE account <http://code.google.com/appengine/>`_ - and create an application. You'll need a mobile phone to accept an - SMS in order to receive authorization. - -#. Edit the application's ID in ``app.yaml`` to match the application - name you created during GAE account setup. - - .. code-block:: yaml - - application: mycoolpyramidapp - -#. Upload the application - - .. code-block:: text - - $ python2.5 /usr/local/bin/appcfg.py update pyramidapp/app - - You almost certainly won't hit the 3000-file GAE file number limit - when invoking this command. If you do, however, it will look like - so: - - .. code-block:: text - - HTTPError: HTTP Error 400: Bad Request - Rolling back the update. - Error 400: --- begin server output --- - Max number of files and blobs is 3000. - --- end server output --- - - If you do experience this error, you will be able to get around - this by zipping libraries. You can use ``pip`` to create zipfiles - from packages. See :ref:`pip_zip` for more information about this. - - A successful upload looks like so: - - .. code-block:: text - - [chrism@vitaminf pyramidapp]$ python2.5 /usr/local/bin/appcfg.py \ - update ../pyramidapp/app/ - Scanning files on local disk. - Scanned 500 files. - # ... more output ... - Will check again in 16 seconds. - Checking if new version is ready to serve. - Closing update: new version is ready to start serving. - Uploading index definitions. - -#. Visit ``http://<yourapp>.appspot.com`` in a browser. - -.. _pip_zip: - -Zipping Files Via Pip ---------------------- - -If you hit the Google App Engine 3000-file limit, you may need to -create zipfile archives out of some distributions installed in your -application's virtualenv. - -First, see which packages are available for zipping: - -.. code-block:: text - - $ bin/pip zip -l - -This shows your zipped packages (by default, none) and your unzipped -packages. You can zip a package like so: - -.. code-block:: text - - $ bin/pip zip pytz-2009g-py2.5.egg - -Note that it requires the whole egg file name. For a :app:`Pyramid` app, the -following packages are good candidates to be zipped. - -- Chameleon -- zope.i18n - -Once the zipping procedure is finished you can try uploading again. diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index 6e3e4ce37..3cc182d13 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -1,18 +1,17 @@ .. _modwsgi_tutorial: Running a :app:`Pyramid` Application under ``mod_wsgi`` -========================================================== +======================================================= :term:`mod_wsgi` is an Apache module developed by Graham Dumpleton. It allows :term:`WSGI` programs to be served using the Apache web server. -This guide will outline broad steps that can be used to get a -:app:`Pyramid` application running under Apache via ``mod_wsgi``. -This particular tutorial was developed under Apple's Mac OS X platform -(Snow Leopard, on a 32-bit Mac), but the instructions should be -largely the same for all systems, delta specific path information for -commands and files. +This guide will outline broad steps that can be used to get a :app:`Pyramid` +application running under Apache via ``mod_wsgi``. This particular tutorial +was developed under Apple's Mac OS X platform (Snow Leopard, on a 32-bit +Mac), but the instructions should be largely the same for all systems, delta +specific path information for commands and files. .. note:: Unfortunately these instructions almost certainly won't work for deploying a :app:`Pyramid` application on a Windows system using @@ -25,21 +24,15 @@ commands and files. system. If you do not, install Apache 2.X for your platform in whatever manner makes sense. +#. It is also assumed that you have satisfied the + :ref:`requirements-for-installing-packages`. + #. Once you have Apache installed, install ``mod_wsgi``. Use the (excellent) `installation instructions <http://code.google.com/p/modwsgi/wiki/InstallationInstructions>`_ for your platform into your system's Apache installation. -#. Install :term:`virtualenv` into the Python which mod_wsgi will - run using the ``easy_install`` program. - - .. code-block:: text - - $ sudo /usr/bin/easy_install-2.6 virtualenv - - This command may need to be performed as the root user. - -#. Create a :term:`virtualenv` which we'll use to install our +#. Create a :term:`virtual environment` which we'll use to install our application. .. code-block:: text @@ -47,14 +40,14 @@ commands and files. $ cd ~ $ mkdir modwsgi $ cd modwsgi - $ /usr/local/bin/virtualenv --no-site-packages env + $ python3 -m venv env -#. Install :app:`Pyramid` into the newly created virtualenv: +#. Install :app:`Pyramid` into the newly created virtual environment: .. code-block:: text $ cd ~/modwsgi/env - $ bin/easy_install pyramid + $ $VENV/bin/pip install pyramid #. Create and install your :app:`Pyramid` application. For the purposes of this tutorial, we'll just be using the ``pyramid_starter`` application as @@ -64,20 +57,21 @@ commands and files. .. code-block:: text $ cd ~/modwsgi/env - $ bin/paster create -t pyramid_starter myapp + $ $VENV/bin/pcreate -s starter myapp $ cd myapp - $ ../bin/python setup.py install + $ $VENV/bin/pip install -e . -#. Within the virtualenv directory (``~/modwsgi/env``), create a +#. Within the virtual environment directory (``~/modwsgi/env``), create a script named ``pyramid.wsgi``. Give it these contents: .. code-block:: python - from pyramid.paster import get_app - application = get_app( - '/Users/chrism/modwsgi/env/myapp/production.ini', 'main') + from pyramid.paster import get_app, setup_logging + ini_path = '/Users/chrism/modwsgi/env/myapp/production.ini' + setup_logging(ini_path) + application = get_app(ini_path, 'main') - The first argument to ``get_app`` is the project Paste configuration file + The first argument to ``get_app`` is the project configuration file name. It's best to use the ``production.ini`` file provided by your scaffold, as it contains settings appropriate for production. The second is the name of the section within the .ini file @@ -85,12 +79,15 @@ commands and files. ``application`` is important: mod_wsgi requires finding such an assignment when it opens the file. -#. Make the ``pyramid.wsgi`` script executable. + The call to ``setup_logging`` initializes the standard library's + `logging` module to allow logging within your application. + See :ref:`logging_config`. - .. code-block:: text - - $ cd ~/modwsgi/env - $ chmod 755 pyramid.wsgi + There is no need to make the ``pyramid.wsgi`` script executable. + However, you'll need to make sure that *two* users have access to change + into the ``~/modwsgi/env`` directory: your current user (mine is + ``chrism`` and the user that Apache will run as often named ``apache`` or + ``httpd``). Make sure both of these users can "cd" into that directory. #. Edit your Apache configuration and add some stuff. I happened to create a file named ``/etc/apache2/other/modwsgi.conf`` on my own @@ -99,12 +96,12 @@ commands and files. .. code-block:: apache # Use only 1 Python sub-interpreter. Multiple sub-interpreters - # play badly with C extensions. + # play badly with C extensions. See + # http://stackoverflow.com/a/10558360/209039 WSGIApplicationGroup %{GLOBAL} WSGIPassAuthorization On - WSGIDaemonProcess pyramid user=chrism group=staff processes=1 \ - threads=4 \ - python-path=/Users/chrism/modwsgi/env/lib/python2.6/site-packages + WSGIDaemonProcess pyramid user=chrism group=staff threads=4 \ + python-path=/Users/chrism/modwsgi/env/lib/python2.7/site-packages WSGIScriptAlias /myapp /Users/chrism/modwsgi/env/pyramid.wsgi <Directory /Users/chrism/modwsgi/env> @@ -128,4 +125,3 @@ serve up a :app:`Pyramid` application. See the `mod_wsgi configuration documentation <http://code.google.com/p/modwsgi/wiki/ConfigurationGuidelines>`_ for more in-depth configuration information. - diff --git a/docs/tutorials/wiki/NOTE-relocatable.txt b/docs/tutorials/wiki/NOTE-relocatable.txt new file mode 100644 index 000000000..e942caba8 --- /dev/null +++ b/docs/tutorials/wiki/NOTE-relocatable.txt @@ -0,0 +1,13 @@ +We specifically use relative package references where possible so this demo +works even if the user names their package (in the '$VENV/bin/pcreate -s +zodb ...' step) something other than 'tutorial'. + +Specifically: + +- use relative imports +- use plain relative URLs for resources (like stylesheets and images) in + page templates. + +Direct uses of the package name, like in __init__.py 'config.scan()' +statements, are already adjusted by the paster/pcreate, so we don't have to +worry about them. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 358c1d5eb..44097b35b 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -1,297 +1,372 @@ +.. _wiki_adding_authorization: + ==================== -Adding Authorization +Adding authorization ==================== -Our application currently allows anyone with access to the server to view, -edit, and add pages to our wiki. For purposes of demonstration we'll change -our application to allow people whom are members of a *group* named -``group:editors`` to add and edit wiki pages but we'll continue allowing -anyone with access to the server to view pages. :app:`Pyramid` provides -facilities for :term:`authorization` and :term:`authentication`. We'll make -use of both features to provide security to our application. +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. We'll make use of both features to provide security to +our application. Our application currently allows anyone with access to the +server to view, edit, and add pages to our wiki. We'll change that to allow +only people who are members of a *group* named ``group:editors`` to add and +edit wiki pages, but we'll continue allowing anyone with access to the server +to view pages. -We will add an :term:`authentication policy` and an -:term:`authorization policy` to our :term:`application -registry`, add a ``security.py`` module and give our :term:`root` -resource an :term:`ACL`. +We will also add a login page and a logout link on all the pages. The login +page will be shown when a user is denied access to any of the views that +require permission, instead of a default "403 Forbidden" page. -Then we will add ``login`` and ``logout`` views, and modify the -existing views to make them return a ``logged_in`` flag to the -renderer and add :term:`permission` declarations to their ``view_config`` -decorators. +We will implement the access control with the following steps: -Finally, we will add a ``login.pt`` template and change the existing -``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. +* Add users and groups (``security.py``, a new module). +* Add an :term:`ACL` (``models.py``). +* Add an :term:`authentication policy` and an :term:`authorization policy` + (``__init__.py``). +* Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` + views (``views.py``). -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/authorization/>`_. +Then we will add the login and logout feature: -Adding Authentication and Authorization Policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Add ``login`` and ``logout`` views (``views.py``). +* Add a login template (``login.pt``). +* Make the existing views return a ``logged_in`` flag to the renderer + (``views.py``). +* Add a "Logout" link to be shown when logged in and viewing or editing a page + (``view.pt``, ``edit.pt``). -We'll change our package's ``__init__.py`` file to enable an -``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable -declarative security checking. We need to import the new policies: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 4-5,8 +Access control +-------------- + +Add users and groups +~~~~~~~~~~~~~~~~~~~~ + +Create a new ``tutorial/security.py`` module with the +following content: + +.. literalinclude:: src/authorization/tutorial/security.py :linenos: :language: python -Then, we'll add those policies to the configuration: +The ``groupfinder`` function accepts a userid and a request and +returns one of these values: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 16-18,26-28 +- If the userid exists in the system, it will return a sequence of group + identifiers (or an empty sequence if the user isn't a member of any groups). +- If the userid *does not* exist in the system, it will return ``None``. + +For example, ``groupfinder('editor', request )`` returns ``['group:editor']``, +``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', +request)`` returns ``None``. We will use ``groupfinder()`` as an +:term:`authentication policy` "callback" that will provide the +:term:`principal` or principals for a user. + +In a production system, user and group data will most often come from a +database, but here we use "dummy" data to represent user and groups sources. + +Add an ACL +~~~~~~~~~~ + +Open ``tutorial/models.py`` and add the following import +statement at the head: + +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 4-7 :linenos: :language: python -Note that the creation of an ``AuthTktAuthenticationPolicy`` requires two -arguments: ``secret`` and ``callback``. ``secret`` is a string representing -an encryption key used by the "authentication ticket" machinery represented -by this policy: it is required. The ``callback`` is a reference to a -``groupfinder`` function in the ``tutorial`` package's ``security.py`` file. -We haven't added that module yet, but we're about to. - -When you're done, your ``__init__.py`` will -look like so: +Add the following lines to the ``Wiki`` class: -.. literalinclude:: src/authorization/tutorial/__init__.py +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 9-13 :linenos: + :lineno-start: 9 + :emphasize-lines: 4-5 :language: python -Adding ``security.py`` -~~~~~~~~~~~~~~~~~~~~~~ +We import :data:`~pyramid.security.Allow`, an action that means that +permission is allowed, and :data:`~pyramid.security.Everyone`, a special +:term:`principal` that is associated to all requests. Both are used in the +:term:`ACE` entries that make up the ACL. -Add a ``security.py`` module within your package (in the same -directory as ``__init__.py``, ``views.py``, etc.) with the following -content: +The ACL is a list that needs to be named `__acl__` and be an attribute of a +class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry +allows any user the `view` permission. The second entry allows the +``group:editors`` principal the `edit` permission. -.. literalinclude:: src/authorization/tutorial/security.py +The ``Wiki`` class that contains the ACL is the :term:`resource` constructor +for the :term:`root` resource, which is a ``Wiki`` instance. The ACL is +provided to each view in the :term:`context` of the request as the ``context`` +attribute. + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually need only *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. See :ref:`assigning_acls` for +more information about what an :term:`ACL` represents. + +Add authentication and authorization policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/__init__.py`` and add the highlighted import +statements: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 1-8 :linenos: + :emphasize-lines: 4-5,8 :language: python -The ``groupfinder`` function defined here is an :term:`authentication policy` -"callback"; it is a callable that accepts a userid and a request. If the -userid exists in the system, the callback will -return a sequence of group identifiers (or an empty sequence if the user -isn't a member of any groups). If the userid *does not* exist in the system, -the callback will return ``None``. In a production system, user and group data will -most often come from a database, but here we use "dummy" data to represent -user and groups sources. Note that the ``editor`` user is a member of the -``group:editors`` group in our dummy group data (the ``GROUPS`` data -structure). +Now add those policies to the configuration: -Giving Our Root Resource an ACL -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 18-23 + :linenos: + :lineno-start: 18 + :emphasize-lines: 1-3,5-6 + :language: python -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Only the highlighted lines need to be added. -We need to perform some imports at module scope in our ``models.py`` file: +We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth +ticket that may be included in the request. We are also enabling an +``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or +*deny* outcome for a view. -.. code-block:: python - :linenos: +Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` +constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is +a string representing an encryption key used by the "authentication ticket" +machinery represented by this policy: it is required. The ``callback`` is the +``groupfinder()`` function that we created before. - from pyramid.security import Allow - from pyramid.security import Everyone +Add permission declarations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Open ``tutorial/views.py`` and add a ``permission='edit'`` parameter +to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 50-52 + :emphasize-lines: 2-3 + :language: python -.. code-block:: python - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 70-72 + :emphasize-lines: 2-3 + :language: python - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] +Only the highlighted lines, along with their preceding commas, need to be +edited and added. -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. +The result is that only users who possess the ``edit`` permission at the time +of the request may invoke those two views. -Our resulting ``models.py`` file will now look like so: +Add a ``permission='view'`` parameter to the ``@view_config`` decorator for +``view_wiki()`` and ``view_page()`` as follows: -.. literalinclude:: src/authorization/tutorial/models.py - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 23-24 + :emphasize-lines: 1-2 :language: python -Adding Login and Logout Views -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 28-29 + :emphasize-lines: 1-2 + :language: python + +Only the highlighted lines, along with their preceding commas, need to be +edited and added. -We'll add a ``login`` view which renders a login form and processes -the post from the login form, checking credentials. +This allows anyone to invoke these two views. -We'll also add a ``logout`` view to our application and provide a link -to it. This view will clear the credentials of the logged in user and +We are done with the changes needed to control access. The changes that +follow will add the login and logout feature. + +Login, logout +------------- + +Add login and logout views +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We'll add a ``login`` view which renders a login form and processes the post +from the login form, checking credentials. + +We'll also add a ``logout`` view callable to our application and provide a +link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -We'll add a different file (for presentation convenience) to add login -and logout views. Add a file named ``login.py`` to your application -(in the same directory as ``views.py``) with the following content: +Add the following import statements to the head of +``tutorial/views.py``: -.. literalinclude:: src/authorization/tutorial/login.py - :linenos: +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 6-17 + :emphasize-lines: 1-12 :language: python -Note that the ``login`` view callable in the ``login.py`` file has *two* view -configuration decorators. The order of these decorators is unimportant. -Each just adds a different :term:`view configuration` for the ``login`` view -callable. - -The first view configuration decorator configures the ``login`` view callable -so it will be invoked when someone visits ``/login`` (when the context is a -Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This -configures our login view to be presented to the user when :app:`Pyramid` -detects that a view invocation can not be authorized. Because we've -configured a forbidden view, the ``login`` view callable will be invoked -whenever one of our users tries to execute a view callable that they are not -allowed to invoke as determined by the :term:`authorization policy` in use. -In our application, for example, this means that if a user has not logged in, -and he tries to add or edit a Wiki page, he will be shown the login form. -Before being allowed to continue on to the add or edit form, he will have to -provide credentials that give him permission to add or edit via this login -form. - -Changing Existing Views -~~~~~~~~~~~~~~~~~~~~~~~ - -Then we need to change each of our ``view_page``, ``edit_page`` and -``add_page`` views in ``views.py`` to pass a "logged in" parameter -into its template. We'll add something like this to each view body: - -.. ignore-next-block -.. code-block:: python - :linenos: +All the highlighted lines need to be added or edited. - from pyramid.security import authenticated_userid - logged_in = authenticated_userid(request) +:meth:`~pyramid.view.forbidden_view_config` will be used to customize the +default 403 Forbidden page. :meth:`~pyramid.security.remember` and +:meth:`~pyramid.security.forget` help to create and expire an auth ticket +cookie. -We'll then change the return value of each view that has an associated -``renderer`` to pass the resulting ``logged_in`` value to the -template. For example: +Now add the ``login`` and ``logout`` views at the end of the file: -.. ignore-next-block -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 82-116 :linenos: + :lineno-start: 82 + :language: python + +``login()`` has two decorators: + +- a ``@view_config`` decorator which associates it with the ``login`` route + and makes it visible when we visit ``/login``, +- a ``@forbidden_view_config`` decorator which turns it into a + :term:`forbidden view`. ``login()`` will be invoked when a user tries to + execute a view callable for which they lack authorization. For example, if + a user has not logged in and tries to add or edit a Wiki page, they will be + shown the login form before being allowed to continue. - return dict(page = context, - content = content, - logged_in = logged_in, - edit_url = edit_url) - -Adding ``permission`` Declarations to our ``view_config`` Decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the - ``view_wiki`` and ``view_page`` view functions. This makes the - assertion that only users who possess the ``view`` permission - against the context resource at the time of the request may - invoke these views. We've granted - :data:`pyramid.security.Everyone` the view permission at the - root model via its ACL, so everyone will be able to invoke the - ``view_wiki`` and ``view_page`` views. - -- We add ``permission='edit'`` to the decorator attached to the - ``add_page`` and ``edit_page`` view functions. This makes the - assertion that only users who possess the effective ``edit`` - permission against the context resource at the time of the - request may invoke these views. We've granted the - ``group:editors`` principal the ``edit`` permission at the - root model via its ACL, so only a user whom is a member of - the group named ``group:editors`` will able to invoke the - ``add_page`` or ``edit_page`` views. We've likewise given - the ``editor`` user membership to this group via the - ``security.py`` file by mapping him to the ``group:editors`` - group in the ``GROUPS`` data structure (``GROUPS - = {'editor':['group:editors']}``); the ``groupfinder`` - function consults the ``GROUPS`` data structure. This means - that the ``editor`` user can add and edit pages. - -Adding the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add a ``login.pt`` template to your templates directory. It's -referred to within the login view we just added to ``login.py``. +The order of these two :term:`view configuration` decorators is unimportant. + +``logout()`` is decorated with a ``@view_config`` decorator which associates +it with the ``logout`` route. It will be invoked when we visit ``/logout``. + +Add the ``login.pt`` Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/login.pt`` with the following content: .. literalinclude:: src/authorization/tutorial/templates/login.pt - :language: xml + :language: html + +The above template is referenced in the login view that we just added in +``views.py``. + +Return a ``logged_in`` flag to the renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Change ``view.pt`` and ``edit.pt`` +Open ``tutorial/views.py`` again. Add a ``logged_in`` parameter to +the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as +follows: + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 47-48 + :emphasize-lines: 1-2 + :language: python + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 67-68 + :emphasize-lines: 1-2 + :language: python + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 78-80 + :emphasize-lines: 2-3 + :language: python + +Only the highlighted lines need to be added or edited. + +The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if +the user is not authenticated, or a userid if the user is authenticated. + +Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We'll also need to change our ``edit.pt`` and ``view.pt`` templates to -display a "Logout" link if someone is logged in. This link will -invoke the logout view. +Open ``tutorial/templates/edit.pt`` and +``tutorial/templates/view.pt`` and add the following code as +indicated by the highlighted lines. -To do so we'll add this to both templates within the ``<div id="right" -class="app-welcome align-right">`` div: +.. literalinclude:: src/authorization/tutorial/templates/edit.pt + :lines: 34-38 + :emphasize-lines: 3-5 + :language: html -.. code-block:: xml +The attribute ``tal:condition="logged_in"`` will make the element be included +when ``logged_in`` is any user id. The link will invoke the logout view. The +above element will not be included if ``logged_in`` is ``None``, such as when +a user is not authenticated. - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> +Reviewing our changes +--------------------- -Seeing Our Changes To ``views.py`` and our Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Our ``tutorial/__init__.py`` will look like this when we're done: -Our ``views.py`` module will look something like this when we're done: +.. literalinclude:: src/authorization/tutorial/__init__.py + :linenos: + :emphasize-lines: 4-5,8,18-20,22-23 + :language: python + +Only the highlighted lines need to be added or edited. + +Our ``tutorial/models.py`` will look like this when we're done: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :emphasize-lines: 4-7,12-13 + :language: python + +Only the highlighted lines need to be added or edited. + +Our ``tutorial/views.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: + :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120 :language: python -Our ``edit.pt`` template will look something like this when we're done: +Only the highlighted lines need to be added or edited. + +Our ``tutorial/templates/edit.pt`` template will look like this when +we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: - :language: xml + :emphasize-lines: 36-38 + :language: html + +Only the highlighted lines need to be added or edited. -Our ``view.pt`` template will look something like this when we're done: +Our ``tutorial/templates/view.pt`` template will look like this when +we're done: .. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: - :language: xml - -Viewing the Application in a Browser -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We can finally examine our application in a browser. The views we'll try are -as follows: - -- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` - view. This always redirects to the ``view_page`` view of the ``FrontPage`` - page resource. It is executable by any user. - -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes the - ``view_page`` view of the ``FrontPage`` Page resource. This is because - it's the :term:`default view` (a view without a ``name``) for ``Page`` - resources. It is executable by any user. - -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes - the edit view for the ``FrontPage`` Page resource. It is executable by - only the ``editor`` user. If a different user (or the anonymous user) - invokes it, a login form will be displayed. Supplying the credentials with - the username ``editor``, password ``editor`` will show the edit page form - being displayed. - -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a page. It is executable by only - the ``editor`` user. If a different user (or the anonymous user) - invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will - show the edit page form being displayed. - -- After logging in (as a result of hitting an edit or add page and - submitting the login form with the ``editor`` credentials), we'll see - a Logout link in the upper right hand corner. When we click it, - we're logged out, and redirected back to the front page. + :emphasize-lines: 36-38 + :language: html + +Only the highlighted lines need to be added or edited. + +Viewing the application in a browser +------------------------------------ + +We can finally examine our application in a browser (See +:ref:`wiki-start-the-application`). Launch a browser and visit each of the +following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` Page resource. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` Page resource. This is because it's the :term:`default view` + (a view without a ``name``) for ``Page`` resources. It is executable by any + user. + +- http://localhost:6543/FrontPage/edit_page invokes the edit view for the + FrontPage object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, a login form will be + displayed. Supplying the credentials with the username ``editor``, password + ``editor`` will display the edit page form. + +- http://localhost:6543/add_page/SomePageName invokes the add view for a page. + It is executable by only the ``editor`` user. If a different user (or the + anonymous user) invokes it, a login form will be displayed. Supplying the + credentials with the username ``editor``, password ``editor`` will display + the edit page form. + +- After logging in (as a result of hitting an edit or add page and submitting + the login form with the ``editor`` credentials), we'll see a Logout link in + the upper right hand corner. When we click it, we're logged out, and + redirected back to the front page. diff --git a/docs/tutorials/wiki/background.rst b/docs/tutorials/wiki/background.rst index e49407b70..31dcd6b53 100644 --- a/docs/tutorials/wiki/background.rst +++ b/docs/tutorials/wiki/background.rst @@ -1,3 +1,5 @@ +.. _wiki_background: + ========== Background ========== @@ -11,8 +13,11 @@ Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* he will need a Windows system of any kind. +variant, etc.) *or* a Windows system of any kind. + +.. warning:: -This tutorial targets :app:`Pyramid` version 1.0. + This tutorial has been written for Python 2. It is unlikely to work + without modification under Python 3. Have fun! diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index 66cf37e4e..20bfdf754 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -1,76 +1,76 @@ +.. _wiki_basic_layout: + ============ Basic Layout ============ -The starter files generated by the ``pyramid_zodb`` scaffold are basic, but +The starter files generated by the ``zodb`` scaffold are very basic, but they provide a good orientation for the high-level patterns common to most -:term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` based) projects. +:term:`traversal`-based (and :term:`ZODB`-based) :app:`Pyramid` projects. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/>`_. -App Startup with ``__init__.py`` --------------------------------- +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. Our application uses ``__init__.py`` as both a package marker, as -well as to contain application configuration code. +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. + +When you run the application using the ``pserve`` command using the +``development.ini`` generated configuration file, the application +configuration points at a setuptools *entry point* described as +``egg:tutorial``. In our application, because the application's ``setup.py`` +file says so, this entry point happens to be the ``main`` function within the +file named ``__init__.py``. -When you run the application using the ``paster`` command using the -``development.ini`` generated config file, the application configuration -points at a Setuptools *entry point* described as ``egg:tutorial``. In our -application, because the application's ``setup.py`` file says so, this entry -point happens to be the ``main`` function within the file named -``__init__.py``: +Open ``tutorial/__init__.py``. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :linenos: + :language: py #. *Lines 1-3*. Perform some dependency imports. -#. *Line 8*. Get the ZODB configuration from the ``development.ini`` - file's ``[app:main]`` section represented by the ``settings`` - dictionary passed to our ``app`` function. This will be a URI - (something like ``file:///path/to/Data.fs``). +#. *Lines 6-8*. Define a :term:`root factory` for our Pyramid application. -#. *Line 12*. We create a "finder" object using the - ``PersistentApplicationFinder`` helper class, passing it the ZODB - URI and the "appmaker" we've imported from ``models.py``. +#. *Line 11*. ``__init__.py`` defines a function named ``main``. -#. *Lines 13 - 14*. We create a :term:`root factory` which uses the - finder to return a ZODB root object. +#. *Line 14*. We construct a :term:`Configurator` with a root + factory and the settings keywords parsed by :term:`PasteDeploy`. The root + factory is named ``root_factory``. -#. *Line 15*. We construct a :term:`Configurator` with a :term:`root - factory` and the settings keywords parsed by PasteDeploy. The root - factory is named ``get_root``. +#. *Line 15*. Include support for the :term:`Chameleon` template rendering + bindings, allowing us to use the ``.pt`` templates. -#. *Line 16*. Register a 'static view' which answers requests which start - with with URL path ``/static`` using the - :meth:`pyramid.config.Configurator.add_static_view method`. This +#. *Line 16*. Register a "static view", which answers requests whose URL + paths start with ``/static``, using the + :meth:`pyramid.config.Configurator.add_static_view` method. This statement registers a view that will serve up static assets, such as CSS and image files, for us, in this case, at ``http://localhost:6543/static/`` and below. The first argument is the "name" ``static``, which indicates that the URL path prefix of the view - will be ``/static``. the The second argument of this tag is the "path", - which is an :term:`asset specification`, so it finds the resources it - should serve within the ``static`` directory inside the ``tutorial`` - package. + will be ``/static``. The second argument of this tag is the "path", + which is a relative :term:`asset specification`, so it finds the resources + it should serve within the ``static`` directory inside the ``tutorial`` + package. Alternatively the scaffold could have used an *absolute* asset + specification as the path (``tutorial:static``). #. *Line 17*. Perform a :term:`scan`. A scan will find :term:`configuration - decoration`, such as view configuration decorators - (e.g. ``@view_config``) in the source code of the ``tutorial`` package and - will take actions based on these decorators. The argument to - :meth:`~pyramid.config.Configurator.scan` is the package name to scan, - which is ``tutorial``. + decoration`, such as view configuration decorators (e.g., ``@view_config``) + in the source code of the ``tutorial`` package and will take actions based + on these decorators. We don't pass any arguments to + :meth:`~pyramid.config.Configurator.scan`, which implies that the scan + should take place in the current package (in this case, ``tutorial``). + The scaffold could have equivalently said ``config.scan('tutorial')``, but + it chose to omit the package name argument. #. *Line 18*. Use the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application. -Resources and Models with ``models.py`` +Resources and models with ``models.py`` --------------------------------------- :app:`Pyramid` uses the word :term:`resource` to describe objects arranged @@ -79,33 +79,33 @@ hierarchically in a :term:`resource tree`. This tree is consulted by tree represents the site structure, but it *also* represents the :term:`domain model` of the application, because each resource is a node stored persistently in a :term:`ZODB` database. The ``models.py`` file is -where the ``pyramid_zodb`` scaffold put the classes that implement our -resource objects, each of which happens also to be a domain model object. +where the ``zodb`` scaffold put the classes that implement our +resource objects, each of which also happens to be a domain model object. Here is the source for ``models.py``: - .. literalinclude:: src/basiclayout/tutorial/models.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models.py + :linenos: + :language: python -#. *Lines 3-4*. The ``MyModel`` :term:`resource` class is implemented here. - Instances of this class will be capable of being persisted in :term:`ZODB` +#. *Lines 4-5*. The ``MyModel`` :term:`resource` class is implemented here. + Instances of this class are capable of being persisted in :term:`ZODB` because the class inherits from the :class:`persistent.mapping.PersistentMapping` class. The ``__parent__`` and ``__name__`` are important parts of the :term:`traversal` protocol. By default, have these as ``None`` indicating that this is the :term:`root` object. -#. *Lines 6-12*. ``appmaker`` is used to return the *application +#. *Lines 8-14*. ``appmaker`` is used to return the *application root* object. It is called on *every request* to the :app:`Pyramid` application. It also performs bootstrapping by *creating* an application root (inside the ZODB root object) if one - does not already exist. + does not already exist. It is used by the ``root_factory`` we've defined + in our ``__init__.py``. - We do so by first seeing if the database has the persistent - application root. If not, we make an instance, store it, and - commit the transaction. We then return the application root - object. + Bootstrapping is done by first seeing if the database has the persistent + application root. If not, we make an instance, store it, and commit the + transaction. We then return the application root object. Views With ``views.py`` ----------------------- @@ -116,19 +116,19 @@ the URL ``http://localhost:6543/``. Here is the source for ``views.py``: - .. literalinclude:: src/basiclayout/tutorial/views.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/views.py + :linenos: + :language: python Let's try to understand the components in this module: #. *Lines 1-2*. Perform some dependency imports. -#. *Line 4*. Use the :func:`pyramid.view.view_config` :term:`configuration +#. *Line 5*. Use the :func:`pyramid.view.view_config` :term:`configuration decoration` to perform a :term:`view configuration` registration. This view configuration registration will be activated when the application is started. It will be activated by virtue of it being found as the result - of a :term:`scan` (when Line 17 of ``__init__.py`` is run). + of a :term:`scan` (when Line 14 of ``__init__.py`` is run). The ``@view_config`` decorator accepts a number of keyword arguments. We use two keyword arguments here: ``context`` and ``renderer``. @@ -140,20 +140,23 @@ Let's try to understand the components in this module: model, this view callable will be invoked. The ``renderer`` argument names an :term:`asset specification` of - ``tutorial:templates/mytemplate.pt``. This asset specification points at - a :term:`Chameleon` template which lives in the ``mytemplate.pt`` file + ``templates/mytemplate.pt``. This asset specification points at a + :term:`Chameleon` template which lives in the ``mytemplate.pt`` file within the ``templates`` directory of the ``tutorial`` package. And indeed if you look in the ``templates`` directory of this package, you'll see a ``mytemplate.pt`` template file, which renders the default home page - of the generated project. + of the generated project. This asset specification is *relative* (to the + view.py's current package). Alternatively we could have used the + absolute asset specification ``tutorial:templates/mytemplate.pt``, but + chose to use the relative version. Since this call to ``@view_config`` doesn't pass a ``name`` argument, the ``my_view`` function which it decorates represents the "default" view callable used when the context is of the type ``MyModel``. -#. *Lines 5-6*. We define a :term:`view callable` named ``my_view``, which +#. *Lines 6-7*. We define a :term:`view callable` named ``my_view``, which we decorated in the step above. This view callable is a *function* we - write generated by the ``pyramid_zodb`` scaffold that is given a + write generated by the ``zodb`` scaffold that is given a ``request`` and which returns a dictionary. The ``mytemplate.pt`` :term:`renderer` named by the asset specification in the step above will convert this dictionary to a :term:`response` on our behalf. @@ -162,45 +165,18 @@ Let's try to understand the components in this module: dictionary is used by the template named by the ``mytemplate.pt`` asset specification to fill in certain values on the page. -The WSGI Pipeline in ``development.ini`` ----------------------------------------- +Configuration in ``development.ini`` +------------------------------------ The ``development.ini`` (in the tutorial :term:`project` directory, as opposed to the tutorial :term:`package` directory) looks like this: -.. literalinclude:: src/views/development.ini - :language: ini - - -Note the existence of a ``[pipeline:main]`` section which specifies our WSGI -pipeline. This "pipeline" will be served up as our WSGI application. As far -as the WSGI server is concerned the pipeline *is* our application. Simpler -configurations don't use a pipeline: instead they expose a single WSGI -application as "main". Our setup is more complicated, so we use a pipeline -composed of :term:`middleware`. - -The ``egg:WebError#evalerror`` middleware is at the "top" of the pipeline. -This is middleware which displays debuggable errors in the browser while -you're developing (not recommended for a production system). - -The ``egg:repoze.zodbconn#closer`` middleware is in the middle of the -pipeline. This is a piece of middleware which closes the ZODB connection -opened by the ``PersistentApplicationFinder`` at the end of the request. - -The ``egg:repoze.retry#retry`` middleware catches ``ConflictError`` -exceptions from ZODB and retries the request up to three times (ZODB is an -optimistic concurrency database that relies on application-level transaction -retries when a conflict occurs). - -The ``tm`` middleware is the last piece of middleware in the pipeline. This -commits a transaction near the end of the request unless there's an exception -raised or the HTTP response code is an error code. The ``tm`` refers to the -``[filter:tm]`` section beneath the pipeline declaration, which configures -the transaction manager. +.. literalinclude:: src/basiclayout/development.ini + :language: ini -The final line in the ``[pipeline:main]`` section is ``tutorial``, which -refers to the ``[app:tutorial]`` section above it. The ``[app:tutorial]`` -section is the section which actually defines our application settings. The -values within this section are passed as ``**settings`` to the ``main`` +Note the existence of a ``[app:main]`` section which specifies our WSGI +application. Our ZODB database settings are specified as the +``zodbconn.uri`` setting within this section. This value, and the other +values within this section, are passed as ``**settings`` to the ``main`` function we defined in ``__init__.py`` when the server is started via -``paster serve``. +``pserve``. diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index baf497458..73dce14d5 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -1,9 +1,11 @@ +.. _wiki_defining_the_domain_model: + ========================= Defining the Domain Model ========================= -The first change we'll make to our stock paster-generated application will be -to define two :term:`resource` constructors, one representing a wiki page, +The first change we'll make to our stock ``pcreate``-generated application will +be to define two :term:`resource` constructors, one representing a wiki page, and another representing the wiki as a mapping of wiki page names to page objects. We'll do this inside our ``models.py`` file. @@ -14,12 +16,9 @@ constructors". Both our Page and Wiki constructors will be class objects. A single instance of the "Wiki" class will serve as a container for "Page" objects, which will be instances of the "Page" class. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/models/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/models/>`_. -Deleting the Database ---------------------- +Delete the database +------------------- In the next step, we're going to remove the ``MyModel`` Python model class from our ``models.py`` file. Since this class is referred to within @@ -30,17 +29,23 @@ directory before proceeding any further. It's always fine to do this as long as you don't care about the content of the database; the database itself will be recreated as necessary. -Making Edits to ``models.py`` ------------------------------ +Edit ``models.py`` +------------------ .. note:: - There is nothing automagically special about the filename ``models.py``. A - project may have many models throughout its codebase in arbitrarily-named - files. Files implementing models often have ``model`` in their filenames, + There is nothing special about the filename ``models.py``. A + project may have many models throughout its codebase in arbitrarily named + files. Files implementing models often have ``model`` in their filenames or they may live in a Python subpackage of your application package named ``models``, but this is only by convention. +Open ``tutorial/models.py`` file and edit it to look like the following: + +.. literalinclude:: src/models/tutorial/models.py + :linenos: + :language: python + The first thing we want to do is remove the ``MyModel`` class from the generated ``models.py`` file. The ``MyModel`` class is only a sample and we're not going to use it. @@ -59,11 +64,11 @@ of the root model is also always ``None``. Then we'll add a ``Page`` class. This class should inherit from the :class:`persistent.Persistent` class. We'll also give it an ``__init__`` method that accepts a single parameter named ``data``. This parameter will -contain the :term:`ReStructuredText` body representing the wiki page content. +contain the :term:`reStructuredText` body representing the wiki page content. Note that ``Page`` objects don't have an initial ``__name__`` or ``__parent__`` attribute. All objects in a traversal graph must have a ``__name__`` and a ``__parent__`` attribute. We don't specify these here -because both ``__name__`` and ``__parent__`` will be set by by a :term:`view` +because both ``__name__`` and ``__parent__`` will be set by a :term:`view` function when a Page is added to our Wiki mapping. As a last step, we want to change the ``appmaker`` function in our @@ -73,28 +78,13 @@ front page) into the Wiki within the ``appmaker``. This will provide :term:`traversal` a :term:`resource tree` to work against when it attempts to resolve URLs to resources. -We're using a mini-framework callable named ``PersistentApplicationFinder`` -in our application (see ``__init__.py``). A ``PersistentApplicationFinder`` -accepts a ZODB URL as well as an "appmaker" callback. This callback -typically lives in the ``models.py`` file. We'll just change this function, -making the necessary edits. - -Looking at the Result of Our Edits to ``models.py`` ---------------------------------------------------- - -The result of all of our edits to ``models.py`` will end up looking -something like this: - -.. literalinclude:: src/models/tutorial/models.py - :linenos: - :language: python - -Viewing the Application in a Browser ------------------------------------- +View the application in a browser +--------------------------------- We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the -application successfully. If you try to start the application, you'll wind +application successfully. If you try to start the application (See +:ref:`wiki-start-the-application`), you'll wind up with a Python traceback on your console that ends with this exception: .. code-block:: text diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index ae4fa6ffb..ac94d8059 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -1,3 +1,5 @@ +.. _wiki_defining_views: + ============== Defining Views ============== @@ -7,7 +9,9 @@ application is typically a simple Python function that accepts two parameters: :term:`context` and :term:`request`. A view callable is assumed to return a :term:`response` object. -.. note:: A :app:`Pyramid` view can also be defined as callable +.. note:: + + A :app:`Pyramid` view can also be defined as callable which accepts *only* a :term:`request` argument. You'll see this one-argument pattern used in other :app:`Pyramid` tutorials and applications. Either calling convention will work in any @@ -15,7 +19,7 @@ assumed to return a :term:`response` object. interchangeably as necessary. In :term:`traversal` based applications, URLs are mapped to a context :term:`resource`, and since our :term:`resource tree` also represents our application's - "domain model", we're often interested in the context, because + "domain model", we're often interested in the context because it represents the persistent storage of our application. For this reason, in this tutorial we define views as callables that accept ``context`` in the callable argument list. If you do @@ -26,45 +30,91 @@ assumed to return a :term:`response` object. We're going to define several :term:`view callable` functions, then wire them into :app:`Pyramid` using some :term:`view configuration`. -The source code for this tutorial stage can be browsed via -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/views/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/views/>`_. Declaring Dependencies in Our ``setup.py`` File =============================================== The view code in our application will depend on a package which is not a dependency of the original "tutorial" application. The original "tutorial" -application was generated by the ``paster create`` command; it doesn't know -about our custom application requirements. We need to add a dependency on -the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by -assigning this dependency to the ``install_requires`` parameter in the -``setup`` function. +application was generated by the ``pcreate`` command; it doesn't know +about our custom application requirements. -Our resulting ``setup.py`` should look like so: +We need to add a dependency on the ``docutils`` package to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. + +Open ``setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py :linenos: + :emphasize-lines: 20 :language: python -.. note:: After these new dependencies are added, you will need to - rerun ``python setup.py develop`` inside the root of the - ``tutorial`` package to obtain and register the newly added - dependency package. +Only the highlighted line needs to be added. + + +Running ``pip install -e .`` +============================ + +Since a new software dependency was added, you will need to run ``pip install +-e .`` again inside the root of the ``tutorial`` package to obtain and register +the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which ``setup.py`` lives) and execute the following command. + +On UNIX: + +.. code-block:: bash + + $ cd tutorial + $ $VENV/bin/pip install -e . + +On Windows: + +.. code-block:: doscon + + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . + +Success executing this command will end with a line to the console something +like: + +.. code-block:: text + + Successfully installed docutils-0.12 tutorial-0.0 -Adding View Functions -===================== -We're going to add four :term:`view callable` functions to our ``views.py`` -module. One view named ``view_wiki`` will display the wiki itself (it will -answer on the root URL), another named ``view_page`` will display an -individual page, another named ``add_page`` will allow a page to be added, -and a final view named ``edit_page`` will allow a page to be edited. +Adding view functions in ``views.py`` +===================================== + +It's time for a major change. Open ``tutorial/views.py`` and edit it to look +like the following: + +.. literalinclude:: src/views/tutorial/views.py + :linenos: + :language: python + +We added some imports and created a regular expression to find "WikiWords". + +We got rid of the ``my_view`` view function and its decorator that was added +when we originally rendered the ``zodb`` scaffold. It was only an example and +isn't relevant to our application. + +Then we added four :term:`view callable` functions to our ``views.py`` +module: + +* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. +* ``view_page()`` - Displays an individual page. +* ``add_page()`` - Allows the user to add a page. +* ``edit_page()`` - Allows the user to edit a page. + +We'll describe each one briefly in the following sections. .. note:: There is nothing special about the filename ``views.py``. A project may - have many view callables throughout its codebase in arbitrarily-named + have many view callables throughout its codebase in arbitrarily named files. Files implementing view callables often have ``view`` in their filenames (or may live in a Python subpackage of your application package named ``views``), but this is only by convention. @@ -72,45 +122,72 @@ and a final view named ``edit_page`` will allow a page to be edited. The ``view_wiki`` view function ------------------------------- -The ``view_wiki`` function will be configured to respond as the default view -callable for a Wiki resource. We'll provide it with a ``@view_config`` -decorator which names the class ``tutorial.models.Wiki`` as its context. -This means that when a Wiki resource is the context, and no :term:`view name` -exists in the request, this view will be used. The view configuration -associated with ``view_wiki`` does not use a ``renderer`` because the view -callable always returns a :term:`response` object rather than a dictionary. -No renderer is necessary when a view returns a response object. - -The ``view_wiki`` view callable always redirects to the URL of a Page -resource named "FrontPage". To do so, it returns an instance of the +Following is the code for the ``view_wiki`` view function and its decorator: + +.. literalinclude:: src/views/tutorial/views.py + :lines: 12-14 + :lineno-start: 12 + :linenos: + :language: python + +.. note:: In our code, we use an *import* that is *relative* to our package + named ``tutorial``, meaning we can omit the name of the package in the + ``import`` and ``context`` statements. In our narrative, however, we refer + to a *class* and thus we use the *absolute* form, meaning that the name of + the package is included. + +``view_wiki()`` is the :term:`default view` that gets called when a request is +made to the root URL of our wiki. It always redirects to an URL which +represents the path to our "FrontPage". + +We provide it with a ``@view_config`` decorator which names the class +``tutorial.models.Wiki`` as its context. This means that when a Wiki resource +is the context and no :term:`view name` exists in the request, then this view +will be used. The view configuration associated with ``view_wiki`` does not +use a ``renderer`` because the view callable always returns a :term:`response` +object rather than a dictionary. No renderer is necessary when a view returns +a response object. + +The ``view_wiki`` view callable always redirects to the URL of a Page resource +named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` -API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` -page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the -"location" of the HTTPFound response, forming an HTTP redirect. +the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.response.Response` does). It uses the +:meth:`pyramid.request.Request.route_url` API to construct an URL to the +``FrontPage`` page resource (i.e., ``http://localhost:6543/FrontPage``), and +uses it as the "location" of the ``HTTPFound`` response, forming an HTTP +redirect. The ``view_page`` view function ------------------------------- -The ``view_page`` function will be configured to respond as the default view -of a Page resource. We'll provide it with a ``@view_config`` decorator which +Here is the code for the ``view_page`` view function and its decorator: + +.. literalinclude:: src/views/tutorial/views.py + :lines: 16-33 + :lineno-start: 16 + :linenos: + :language: python + +The ``view_page`` function is configured to respond as the default view +of a Page resource. We provide it with a ``@view_config`` decorator which names the class ``tutorial.models.Page`` as its context. This means that when a Page resource is the context, and no :term:`view name` exists in the request, this view will be used. We inform :app:`Pyramid` this view will use the ``templates/view.pt`` template file as a ``renderer``. -The ``view_page`` function generates the :term:`ReStructuredText` body of a +The ``view_page`` function generates the :term:`reStructuredText` body of a page (stored as the ``data`` attribute of the context passed to the view; the -context will be a Page resource) as HTML. Then it substitutes an HTML anchor -for each *WikiWord* reference in the rendered HTML using a compiled regular -expression. +context will be a ``Page`` resource) as HTML. Then it substitutes an HTML +anchor for each *WikiWord* reference in the rendered HTML using a compiled +regular expression. The curried function named ``check`` is used as the first argument to ``wikiwords.sub``, indicating that it should be called to provide a value for each WikiWord match found in the content. If the wiki (our page's ``__parent__``) already contains a page with the matched WikiWord name, the ``check`` function generates a view link to be used as the substitution value -and returns it. If the wiki does not already contain a page with with the +and returns it. If the wiki does not already contain a page with the matched WikiWord name, the function generates an "add" link as the substitution value and returns it. @@ -118,8 +195,8 @@ As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of our current page resource. -We then generate an edit URL (because it's easier to do here than in the -template), and we wrap up a number of arguments in a dictionary and return +We then generate an edit URL because it's easier to do here than in the +template, and we wrap up a number of arguments in a dictionary and return it. The arguments we wrap into a dictionary include ``page``, ``content``, and @@ -138,15 +215,23 @@ callable. In the ``view_wiki`` view callable, we unconditionally return a The ``add_page`` view function ------------------------------ -The ``add_page`` function will be configured to respond when the context -resource is a Wiki and the :term:`view name` is ``add_page``. We'll provide -it with a ``@view_config`` decorator which names the string ``add_page`` as -its :term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its -context, and the renderer named ``templates/edit.pt``. This means that when -a Wiki resource is the context, and a :term:`view name` named ``add_page`` +Here is the code for the ``add_page`` view function and its decorator: + +.. literalinclude:: src/views/tutorial/views.py + :lines: 35-50 + :lineno-start: 35 + :linenos: + :language: python + +The ``add_page`` function is configured to respond when the context resource +is a Wiki and the :term:`view name` is ``add_page``. We provide it with a +``@view_config`` decorator which names the string ``add_page`` as its +:term:`view name` (via ``name=``), the class ``tutorial.models.Wiki`` as its +context, and the renderer named ``templates/edit.pt``. This means that when a +Wiki resource is the context, and a :term:`view name` named ``add_page`` exists as the result of traversal, this view will be used. We inform -:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as -a ``renderer``. We share the same template between add and edit views, thus +:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a +``renderer``. We share the same template between add and edit views, thus ``edit.pt`` instead of ``add.pt``. The ``add_page`` function will be invoked when a user clicks on a WikiWord @@ -159,7 +244,7 @@ Page resource). The request :term:`subpath` in :app:`Pyramid` is the sequence of names that are found *after* the :term:`view name` in the URL segments given in the ``PATH_INFO`` of the WSGI request as the result of :term:`traversal`. If our -add view is invoked via, e.g. ``http://localhost:6543/add_page/SomeName``, +add view is invoked via, e.g., ``http://localhost:6543/add_page/SomeName``, the :term:`subpath` will be a tuple: ``('SomeName',)``. The add view takes the zeroth element of the subpath (the wiki page name), @@ -169,14 +254,14 @@ we're trying to add. If the view rendering is *not* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``False``), the view renders a template. To do so, it generates a "save url" which the template -use as the form post URL during rendering. We're lazy here, so we're trying +uses as the form post URL during rendering. We're lazy here, so we're trying to use the same template (``templates/edit.pt``) for the add view as well as the page edit view. To do so, we create a dummy Page resource object in order to satisfy the edit form's desire to have *some* page object exposed as ``page``, and we'll render the template to a response. If the view rendering *is* a result of a form submission (if the expression -``'form.submitted' in request.params`` is ``True``), we scrape the page body +``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object using the name in the subpath and the page body, and save it into "our context" (the Wiki) using the ``__setitem__`` method of the context. We then redirect back to the @@ -185,8 +270,16 @@ the page body, and save it into "our context" (the Wiki) using the The ``edit_page`` view function ------------------------------- -The ``edit_page`` function will be configured to respond when the context is -a Page resource and the :term:`view name` is ``edit_page``. We'll provide it +Here is the code for the ``edit_page`` view function and its decorator: + +.. literalinclude:: src/views/tutorial/views.py + :lines: 52-60 + :lineno-start: 52 + :linenos: + :language: python + +The ``edit_page`` function is configured to respond when the context is +a Page resource and the :term:`view name` is ``edit_page``. We provide it with a ``@view_config`` decorator which names the string ``edit_page`` as its :term:`view name` (via ``name=``), the class ``tutorial.models.Page`` as its context, and the renderer named ``templates/edit.pt``. This means that when @@ -211,110 +304,95 @@ If the view execution *is* a result of a form submission (if the expression attribute of the page context. It then redirects to the default view of the context (the page), which will always be the ``view_page`` view. -Viewing the Result of all Our Edits to ``views.py`` -=================================================== +Adding templates +================ -The result of all of our edits to ``views.py`` will leave it looking like -this: +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Chameleon` +:term:`ZPT` template. These templates will live in the ``templates`` +directory of our tutorial package. Chameleon templates must have a ``.pt`` +extension to be recognized as such. -.. literalinclude:: src/views/tutorial/views.py - :linenos: - :language: python +The ``view.pt`` template +------------------------ -Adding Templates -================ +Create ``tutorial/templates/view.pt`` and add the following +content: + +.. literalinclude:: src/views/tutorial/templates/view.pt + :linenos: + :language: html -Most view callables we've added expected to be rendered via a -:term:`template`. The default templating systems in :app:`Pyramid` are -:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of :term:`ZPT`, -which is an XML-based templating language. Mako is a non-XML-based -templating language. Because we had to pick one, we chose Chameleon for this -tutorial. +This template is used by ``view_page()`` for displaying a single +wiki page. It includes: -The templates we create will live in the ``templates`` directory of our -tutorial package. Chameleon templates must have a ``.pt`` extension to be -recognized as such. +- A ``div`` element that is replaced with the ``content`` value provided by + the view (lines 36-38). ``content`` contains HTML, so the ``structure`` + keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.) +- A link that points at the "edit" URL which invokes the ``edit_page`` view + for the page being viewed (lines 40-42). -The ``view.pt`` Template +The ``edit.pt`` template ------------------------ -The ``view.pt`` template is used for viewing a single Page. It is used by -the ``view_page`` view function. It should have a div that is "structure -replaced" with the ``content`` value provided by the view. It should also -have a link on the rendered page that points at the "edit" URL (the URL which -invokes the ``edit_page`` view for the page being viewed). +Create ``tutorial/templates/edit.pt`` and add the following content: -Once we're done with the ``view.pt`` template, it will look a lot like -the below: +.. literalinclude:: src/views/tutorial/templates/edit.pt + :linenos: + :language: html -.. literalinclude:: src/views/tutorial/templates/view.pt - :language: xml - -.. note:: The names available for our use in a template are always those that - are present in the dictionary returned by the view callable. But our - templates make use of a ``request`` object that none of our tutorial views - return in their dictionary. This value appears as if "by magic". - However, ``request`` is one of several names that are available "by - default" in a template when a template renderer is used. See - :ref:`chameleon_template_renderers` for more information about other names - that are available by default in a template when a template is used as a - renderer. - -The ``edit.pt`` Template ------------------------- +This template is used by ``add_page()`` and ``edit_page()`` for adding and +editing a wiki page. It displays a page containing a form that includes: -The ``edit.pt`` template is used for adding and editing a Page. It is used -by the ``add_page`` and ``edit_page`` view functions. It should display a -page containing a form that POSTs back to the "save_url" argument supplied by -the view. The form should have a "body" textarea field (the page data), and -a submit button that has the name "form.submitted". The textarea in the form -should be filled with any existing page data when it is rendered. +- A 10 row by 60 column ``textarea`` field named ``body`` that is filled + with any existing page data when it is rendered (line 45). +- A submit button that has the name ``form.submitted`` (line 48). -Once we're done with the ``edit.pt`` template, it will look a lot like the -below: +The form POSTs back to the ``save_url`` argument supplied by the view (line +43). The view will use the ``body`` and ``form.submitted`` values. -.. literalinclude:: src/views/tutorial/templates/edit.pt - :language: xml +.. note:: Our templates use a ``request`` object that none of our tutorial + views return in their dictionary. ``request`` is one of several names that + are available "by default" in a template when a template renderer is used. + See :ref:`renderer_system_values` for information about other names that + are available by default when a template is used as a renderer. -Static Assets + +Static assets ------------- -Our templates name a single static asset named ``pylons.css``. We don't need -to create this file within our package's ``static`` directory because it was -provided at the time we created the project. This file is a little too long to -replicate within the body of this guide, however it is available `online -<http://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki/src/views/tutorial/static/pylons.css>`_. +Our templates name static assets, including CSS and images. We don't need +to create these files within our package's ``static`` directory because they +were provided at the time we created the project. -This CSS file will be accessed via -e.g. ``http://localhost:6543/static/pylons.css`` by virtue of the call to +As an example, the CSS file will be accessed via +``http://localhost:6543/static/theme.css`` by virtue of the call to the ``add_static_view`` directive we've made in the ``__init__.py`` file. Any number and type of static assets can be placed in this directory (or subdirectories) and are just referred to by URL or by using the convenience -method ``static_url`` e.g. ``request.static_url('{{package}}:static/foo.css')`` -within templates. +method ``static_url``, e.g., +``request.static_url('<package>:static/foo.css')`` within templates. + -Viewing the Application in a Browser +Viewing the application in a browser ==================================== -We can finally examine our application in a -browser. The views we'll try are as follows: +We can finally examine our application in a browser (See +:ref:`wiki-start-the-application`). Launch a browser and visit +each of the following URLs, checking that the result is as expected: -- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` - view. This always redirects to the ``view_page`` view of the ``FrontPage`` - Page resource. +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes - the ``view_page`` view of the front page resource. This is - because it's the *default view* (a view without a ``name``) for Page - resources. +- http://localhost:6543/FrontPage/ invokes the ``view_page`` view of the front + page resource. This is because it's the :term:`default view` (a view + without a ``name``) for Page resources. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser - invokes the edit view for the ``FrontPage`` Page resource. +- http://localhost:6543/FrontPage/edit_page invokes the edit view for the + ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a Page. +- http://localhost:6543/add_page/SomePageName invokes the add view for a Page. -- To generate an error, visit ``http://localhost:6543/add_page`` which - will generate an ``IndexError`` for the expression - ``request.subpath[0]``. You'll see an interactive traceback - facility provided by :term:`WebError`. +- To generate an error, visit http://localhost:6543/add_page which will + generate an ``IndexErrorr: tuple index out of range`` error. You'll see an + interactive traceback facility provided by :term:`pyramid_debugtoolbar`. diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst new file mode 100644 index 000000000..f2a02176b --- /dev/null +++ b/docs/tutorials/wiki/design.rst @@ -0,0 +1,151 @@ +.. _wiki_design: + +====== +Design +====== + +Following is a quick overview of the design of our wiki application, to help +us understand the changes that we will be making as we work through the +tutorial. + +Overall +------- + +We choose to use :term:`reStructuredText` markup in the wiki text. Translation +from reStructuredText to HTML is provided by the widely used ``docutils`` +Python module. We will add this module in the dependency list on the project +``setup.py`` file. + +Models +------ + +The root resource named ``Wiki`` will be a mapping of wiki page +names to page resources. The page resources will be instances +of a *Page* class and they store the text content. + +URLs like ``/PageName`` will be traversed using Wiki[ +*PageName* ] => page, and the context that results is the page +resource of an existing page. + +To add a page to the wiki, a new instance of the page resource +is created and its name and reference are added to the Wiki +mapping. + +A page named ``FrontPage`` containing the text *This is the front page*, will +be created when the storage is initialized, and will be used as the wiki home +page. + +Views +----- + +There will be three views to handle the normal operations of adding, +editing, and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both adding +and editing wiki pages. + +The default templating systems in :app:`Pyramid` are +:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of +:term:`ZPT`, which is an XML-based templating language. Mako is a +non-XML-based templating language. Because we had to pick one, +we chose Chameleon for this tutorial. + +Security +-------- + +We'll eventually be adding security to our application. The components we'll +use to do this are below. + +- USERS, a dictionary mapping :term:`userids <userid>` to their + corresponding passwords. + +- GROUPS, a dictionary mapping :term:`userids <userid>` to a + list of groups to which they belong. + +- ``groupfinder``, an *authorization callback* that looks up USERS and + GROUPS. It will be provided in a new ``security.py`` file. + +- An :term:`ACL` is attached to the root :term:`resource`. Each row below + details an :term:`ACE`: + + +----------+----------------+----------------+ + | Action | Principal | Permission | + +==========+================+================+ + | Allow | Everyone | View | + +----------+----------------+----------------+ + | Allow | group:editors | Edit | + +----------+----------------+----------------+ + +- Permission declarations are added to the views to assert the security + policies as each request is handled. + +Two additional views and one template will handle the login and +logout tasks. + +Summary +------- + +The URL, context, actions, template and permission associated to each view are +listed in the following table: + ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| URL | View | Context | Action | Template | Permission | +| | | | | | | ++======================+=============+=================+=======================+============+============+ +| / | view_wiki | Wiki | Redirect to | | | +| | | | /FrontPage | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| /PageName | view_page | Page | Display existing | view.pt | view | +| | [1]_ | | page [2]_ | | | +| | | | | | | +| | | | | | | +| | | | | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| /PageName/edit_page | edit_page | Page | Display edit form | edit.pt | edit | +| | | | with existing | | | +| | | | content. | | | +| | | | | | | +| | | | If the form was | | | +| | | | submitted, redirect | | | +| | | | to /PageName | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| /add_page/PageName | add_page | Wiki | Create the page | edit.pt | edit | +| | | | *PageName* in | | | +| | | | storage, display | | | +| | | | the edit form | | | +| | | | without content. | | | +| | | | | | | +| | | | If the form was | | | +| | | | submitted, | | | +| | | | redirect to | | | +| | | | /PageName | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| /login | login | Wiki, | Display login form. | login.pt | | +| | | Forbidden [3]_ | | | | +| | | | If the form was | | | +| | | | submitted, | | | +| | | | authenticate. | | | +| | | | | | | +| | | | - If authentication | | | +| | | | succeeds, | | | +| | | | redirect to the | | | +| | | | page that we | | | +| | | | came from. | | | +| | | | | | | +| | | | - If authentication | | | +| | | | fails, display | | | +| | | | login form with | | | +| | | | "login failed" | | | +| | | | message. | | | +| | | | | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ +| /logout | logout | Wiki | Redirect to | | | +| | | | /FrontPage | | | ++----------------------+-------------+-----------------+-----------------------+------------+------------+ + +.. [1] This is the default view for a Page context + when there is no view name. +.. [2] Pyramid will return a default 404 Not Found page + if the page *PageName* does not exist yet. +.. [3] ``pyramid.exceptions.Forbidden`` is reached when a + user tries to invoke a view that is + not authorized by the authorization policy. diff --git a/docs/tutorials/wiki/distributing.rst b/docs/tutorials/wiki/distributing.rst index ed0af222f..c3037f396 100644 --- a/docs/tutorials/wiki/distributing.rst +++ b/docs/tutorials/wiki/distributing.rst @@ -1,42 +1,41 @@ +.. _wiki_distributing_your_application: + ============================= Distributing Your Application ============================= -Once your application works properly, you can create a "tarball" from -it by using the ``setup.py sdist`` command. The following commands -assume your current working directory is the ``tutorial`` package -we've created and that the parent directory of the ``tutorial`` -package is a virtualenv representing a :app:`Pyramid` environment. +Once your application works properly, you can create a "tarball" from it by +using the ``setup.py sdist`` command. The following commands assume your +current working directory is the ``tutorial`` package we've created and that +the parent directory of the ``tutorial`` package is a virtual environment +representing a :app:`Pyramid` environment. On UNIX: -.. code-block:: text +.. code-block:: bash - $ ../bin/python setup.py sdist + $ $VENV/bin/python setup.py sdist On Windows: -.. code-block:: text +.. code-block:: doscon - c:\pyramidtut> ..\Scripts\python setup.py sdist + c:\pyramidtut> %VENV%\Scripts\python setup.py sdist The output of such a command will be something like: .. code-block:: text running sdist - # .. more output .. + # more output creating dist - tar -cf dist/tutorial-0.1.tar tutorial-0.1 - gzip -f9 dist/tutorial-0.1.tar - removing 'tutorial-0.1' (and everything under it) - -Note that this command creates a tarball in the "dist" subdirectory -named ``tutorial-0.1.tar.gz``. You can send this file to your friends -to show them your cool new application. They should be able to -install it by pointing the ``easy_install`` command directly at it. -Or you can upload it to `PyPI <http://pypi.python.org>`_ and share it -with the rest of the world, where it can be downloaded via -``easy_install`` remotely like any other package people download from -PyPI. - + Creating tar archive + removing 'tutorial-0.0' (and everything under it) + +Note that this command creates a tarball in the "dist" subdirectory named +``tutorial-0.0.tar.gz``. You can send this file to your friends to show them +your cool new application. They should be able to install it by pointing the +``pip install .`` command directly at it. Or you can upload it to `PyPI +<http://pypi.python.org>`_ and share it with the rest of the world, where it +can be downloaded via ``pip install`` remotely like any other package people +download from PyPI. diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst index 3edc6ba04..7808c7623 100644 --- a/docs/tutorials/wiki/index.rst +++ b/docs/tutorials/wiki/index.rst @@ -3,21 +3,22 @@ ZODB + Traversal Wiki Tutorial ============================== -This tutorial introduces a :term:`traversal` -based :app:`Pyramid` -application to a developer familiar with Python. It will be most familiar to -developers with previous :term:`Zope` experience. When we're done with the -tutorial, the developer will have created a basic Wiki application with +This tutorial introduces a :term:`ZODB` and :term:`traversal`-based +:app:`Pyramid` application to a developer familiar with Python. It will be +most familiar to developers with previous :term:`Zope` experience. When the +is finished, the developer will have created a basic Wiki application with authentication. For cut and paste purposes, the source code for all stages of this -tutorial can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/>`_. +tutorial can be browsed on GitHub at `docs/tutorials/wiki/src +<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src>`_, +which corresponds to the same location if you have Pyramid sources. .. toctree:: :maxdepth: 2 background + design installation basiclayout definingmodels @@ -25,4 +26,3 @@ tutorial can be browsed at authorization tests distributing - diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 30fb67441..dbf995595 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -1,269 +1,394 @@ +.. _wiki_installation: + ============ Installation ============ -For the most part, the installation process for this tutorial -duplicates the steps described in :ref:`installing_chapter` and -:ref:`project_narr`, however it also explains how to install -additional libraries for tutorial purposes. +Before you begin +---------------- + +This tutorial assumes that you have already followed the steps in +:ref:`installing_chapter`, except **do not create a virtual environment or +install Pyramid**. Thereby you will satisfy the following requirements. + +* A Python interpreter is installed on your operating system. +* You've satisfied the :ref:`requirements-for-installing-packages`. + -Preparation -======================== +Create directory to contain the project +--------------------------------------- -Please take the following steps to prepare for the tutorial. The -steps to prepare for the tutorial are slightly different depending on -whether you're using UNIX or Windows. +We need a workspace for our project files. -Preparation, UNIX ------------------ +On UNIX +^^^^^^^ -#. If you don't already have a Python 2.6 interpreter installed on - your system, obtain, install, or find `Python 2.6 - <http://python.org/download/releases/2.6.6/>`_ for your system. +.. code-block:: bash -#. Make sure the Python development headers are installed on your system. If - you've installed Python from source, these will already be installed. If - you're using a system Python, you may have to install a ``python-dev`` - package (e.g. ``apt-get python-dev``). The headers are not required for - Pyramid itself, just for dependencies of the tutorial. + $ mkdir ~/pyramidtut -#. Install the latest `setuptools` into the Python you - obtained/installed/found in the step above: download `ez_setup.py - <http://peak.telecommunity.com/dist/ez_setup.py>`_ and run it using - the ``python`` interpreter of your Python 2.6 installation: +On Windows +^^^^^^^^^^ - .. code-block:: text +.. code-block:: doscon - $ /path/to/my/Python-2.6/bin/python ez_setup.py + c:\> mkdir pyramidtut -#. Use that Python's `bin/easy_install` to install `virtualenv`: - .. code-block:: text +Create and use a virtual Python environment +------------------------------------------- - $ /path/to/my/Python-2.6/bin/easy_install virtualenv +Next let's create a virtual environment workspace for our project. We will use +the ``VENV`` environment variable instead of the absolute path of the virtual +environment. -#. Use that Python's virtualenv to make a workspace: +On UNIX +^^^^^^^ - .. code-block:: text +.. code-block:: bash - $ path/to/my/Python-2.6/bin/virtualenv --no-site-packages \ - pyramidtut + $ export VENV=~/pyramidtut + $ python3 -m venv $VENV -#. Switch to the ``pyramidtut`` directory: +On Windows +^^^^^^^^^^ - .. code-block:: text +.. code-block:: doscon - $ cd pyramidtut + c:\> set VENV=c:\pyramidtut -#. (Optional) Consider using ``source bin/activate`` to make your - shell environment wired to use the virtualenv. +Each version of Python uses different paths, so you will need to adjust the +path to the command for your Python version. -#. Use ``easy_install`` to get :app:`Pyramid` and its direct - dependencies installed: +Python 2.7: - .. code-block:: text +.. code-block:: doscon - $ bin/easy_install pyramid + c:\> c:\Python27\Scripts\virtualenv %VENV% -#. Use ``easy_install`` to install ``docutils``, ``repoze.tm2``, - ``repoze.zodbconn``, ``nose`` and ``coverage``: +Python 3.5: - .. code-block:: text +.. code-block:: doscon - $ bin/easy_install docutils repoze.tm2 repoze.zodbconn \ - nose coverage + c:\> c:\Python35\Scripts\python -m venv %VENV% -Preparation, Windows --------------------- -#. Install, or find `Python 2.6 - <http://python.org/download/releases/2.6.6/>`_ for your system. +Upgrade ``pip`` and ``setuptools`` in the virtual environment +------------------------------------------------------------- -#. Install the latest `setuptools` into the Python you - obtained/installed/found in the step above: download `ez_setup.py - <http://peak.telecommunity.com/dist/ez_setup.py>`_ and run it using - the ``python`` interpreter of your Python 2.6 installation using a - command prompt: +On UNIX +^^^^^^^ - .. code-block:: text +.. code-block:: bash - c:\> c:\Python26\python ez_setup.py + $ $VENV/bin/pip install --upgrade pip setuptools -#. Use that Python's `bin/easy_install` to install `virtualenv`: +On Windows +^^^^^^^^^^ - .. code-block:: text +.. code-block:: doscon - c:\> c:\Python26\Scripts\easy_install virtualenv + c:\> %VENV%\Scripts\pip install --upgrade pip setuptools -#. Use that Python's virtualenv to make a workspace: - .. code-block:: text +Install Pyramid into the virtual Python environment +--------------------------------------------------- - c:\> c:\Python26\Scripts\virtualenv --no-site-packages pyramidtut +On UNIX +^^^^^^^ -#. Switch to the ``pyramidtut`` directory: +.. code-block:: bash - .. code-block:: text + $ $VENV/bin/pip install pyramid - c:\> cd pyramidtut +On Windows +^^^^^^^^^^ -#. (Optional) Consider using ``bin\activate.bat`` to make your shell - environment wired to use the virtualenv. +.. code-block:: doscon -#. Use ``easy_install`` to get :app:`Pyramid` and its direct - dependencies installed: + c:\> %VENV%\Scripts\pip install pyramid - .. code-block:: text +Change directory to your virtual Python environment +--------------------------------------------------- - c:\pyramidtut> Scripts\easy_install pyramid +Change directory to the ``pyramidtut`` directory, which is both your workspace +and your virtual environment. -#. Use ``easy_install`` to install ``docutils``, ``repoze.tm2``, - ``repoze.zodbconn``, ``nose`` and ``coverage``: +On UNIX +^^^^^^^ - .. code-block:: text +.. code-block:: bash - c:\pyramidtut> Scripts\easy_install docutils repoze.tm2 ^ - repoze.zodbconn nose coverage + $ cd pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\> cd pyramidtut .. _making_a_project: -Making a Project -================ +Making a project +---------------- -Your next step is to create a project. :app:`Pyramid` supplies a -variety of scaffolds to generate sample projects. For this tutorial, -we will use the :term:`ZODB` -oriented scaffold named ``pyramid_zodb``. +Your next step is to create a project. For this tutorial, we will use +the :term:`scaffold` named ``zodb``, which generates an application +that uses :term:`ZODB` and :term:`traversal`. -The below instructions assume your current working directory is the -"virtualenv" named "pyramidtut". +:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We +will use ``pcreate``, a script that comes with Pyramid, to create our project +using a scaffold. -On UNIX: +By passing ``zodb`` into the ``pcreate`` command, the script creates the files +needed to use ZODB. By passing in our application name ``tutorial``, the script +inserts that application name into all the required files. -.. code-block:: text +The below instructions assume your current working directory is "pyramidtut". - $ bin/paster create -t pyramid_zodb tutorial +On UNIX +^^^^^^^ -On Windows: +.. code-block:: bash -.. code-block:: text + $ $VENV/bin/pcreate -s zodb tutorial - c:\pyramidtut> Scripts\paster create -t pyramid_zodb tutorial +On Windows +^^^^^^^^^^ -.. note:: If you are using Windows, the ``pyramid_zodb`` Paster scaffold - doesn't currently deal gracefully with installation into a location - that contains spaces in the path. If you experience startup - problems, try putting both the virtualenv and the project into - directories that do not contain spaces in their paths. +.. code-block:: doscon -Installing the Project in "Development Mode" -============================================ + c:\pyramidtut> %VENV%\Scripts\pcreate -s zodb tutorial -In order to do development on the project easily, you must "register" -the project as a development egg in your workspace using the -``setup.py develop`` command. In order to do so, cd to the "tutorial" -directory you created in :ref:`making_a_project`, and run the -"setup.py develop" command using virtualenv Python interpreter. +.. note:: If you are using Windows, the ``zodb`` scaffold may not deal + gracefully with installation into a location that contains spaces in the + path. If you experience startup problems, try putting both the virtual + environment and the project into directories that do not contain spaces in + their paths. -On UNIX: -.. code-block:: text +.. _installing_project_in_dev_mode_zodb: - $ cd tutorial - $ ../bin/python setup.py develop +Installing the project in development mode +------------------------------------------ -On Windows: +In order to do development on the project easily, you must "register" the +project as a development egg in your workspace using the ``pip install -e .`` +command. In order to do so, change directory to the ``tutorial`` directory that +you created in :ref:`making_a_project`, and run the ``pip install -e .`` +command using the virtual environment Python interpreter. -.. code-block:: text +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ cd tutorial + $ $VENV/bin/pip install -e . + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . + +The console will show ``pip`` checking for packages and installing missing +packages. Success executing this command will show a line like the following: + +.. code-block:: bash + + Successfully installed BTrees-4.2.0 Chameleon-2.24 Mako-1.0.4 \ + MarkupSafe-0.23 Pygments-2.1.3 ZConfig-3.1.0 ZEO-4.2.0b1 ZODB-4.2.0 \ + ZODB3-3.11.0 mock-2.0.0 pbr-1.8.1 persistent-4.1.1 pyramid-chameleon-0.3 \ + pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \ + pyramid-zodbconn-0.7 six-1.10.0 transaction-1.4.4 tutorial waitress-0.8.10 \ + zc.lockfile-1.1.0 zdaemon-4.1.0 zodbpickle-0.6.0 zodburi-2.0 + + +.. _install-testing-requirements_zodb: + +Install testing requirements +---------------------------- + +In order to run tests, we need to install the testing requirements. This is +done through our project's ``setup.py`` file, in the ``tests_require`` and +``extras_require`` stanzas, and by issuing the command below for your +operating system. + +.. literalinclude:: src/installation/setup.py + :language: python + :linenos: + :lineno-start: 22 + :lines: 22-26 + +.. literalinclude:: src/installation/setup.py + :language: python + :linenos: + :lineno-start: 45 + :lines: 45-47 + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/pip install -e ".[testing]" + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]" - C:\pyramidtut> cd tutorial - C:\pyramidtut\tutorial> ..\Scripts\python setup.py develop .. _running_tests: -Running the Tests -================= +Run the tests +------------- -After you've installed the project in development mode, you may run -the tests for the project. +After you've installed the project in development mode as well as the testing +requirements, you may run the tests for the project. -On UNIX: +On UNIX +^^^^^^^ -.. code-block:: text +.. code-block:: bash - $ ../bin/python setup.py test -q + $ $VENV/bin/py.test tutorial/tests.py -q -On Windows: +On Windows +^^^^^^^^^^ -.. code-block:: text +.. code-block:: doscon + + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q + +For a successful test run, you should see output that ends like this: + +.. code-block:: bash + + . + 1 passed in 0.24 seconds + + +Expose test coverage information +-------------------------------- + +You can run the ``py.test`` command to see test coverage information. This +runs the tests in the same way that ``py.test`` does, but provides additional +"coverage" information, exposing which lines of your project are covered by the +tests. + +We've already installed the ``pytest-cov`` package into our virtual +environment, so we can run the tests with coverage. + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/py.test --cov=tutorial --cov-report=term-missing tutorial/tests.py + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \ + --cov-report=term-missing tutorial\tests.py -Starting the Application -======================== +If successful, you will see output something like this: + +.. code-block:: bash + + ======================== test session starts ======================== + platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: + plugins: cov-2.2.1 + collected 1 items + + tutorial/tests.py . + ------------------ coverage: platform Python 3.5.1 ------------------ + Name Stmts Miss Cover Missing + ---------------------------------------------------- + tutorial/__init__.py 12 7 42% 7-8, 14-18 + tutorial/models.py 10 6 40% 9-14 + tutorial/tests.py 12 0 100% + tutorial/views.py 4 0 100% + ---------------------------------------------------- + TOTAL 38 13 66% + + ===================== 1 passed in 0.31 seconds ====================== + +Our package doesn't quite have 100% test coverage. + + +.. _wiki-start-the-application: + +Start the application +--------------------- Start the application. -On UNIX: +On UNIX +^^^^^^^ -.. code-block:: text +.. code-block:: bash - $ ../bin/paster serve development.ini --reload + $ $VENV/bin/pserve development.ini --reload -On Windows: +On Windows +^^^^^^^^^^ -.. code-block:: text +.. code-block:: doscon - c:\pyramidtut\tutorial> ..\Scripts\paster serve development.ini --reload + c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload -Exposing Test Coverage Information -================================== +.. note:: -You can run the ``nosetests`` command to see test coverage -information. This runs the tests in the same way that ``setup.py -test`` does but provides additional "coverage" information, exposing -which lines of your project are "covered" (or not covered) by the -tests. + Your OS firewall, if any, may pop up a dialog asking for authorization + to allow python to accept incoming network connections. -On UNIX: +If successful, you will see something like this on your console: .. code-block:: text - $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage + Starting subprocess with file monitor + Starting server in PID 95736. + serving on http://127.0.0.1:6543 -On Windows: +This means the server is ready to accept requests. -.. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ - --cover-erase --with-coverage +Visit the application in a browser +---------------------------------- -Looks like the code in the ``pyramid_zodb`` scaffold for ZODB projects is -missing some test coverage, particularly in the file named -``models.py``. +In a browser, visit http://localhost:6543/. You will see the generated +application's default page. -Visit the Application in a Browser -================================== +One thing you'll notice is the "debug toolbar" icon on right hand side of the +page. You can read more about the purpose of the icon at +:ref:`debug_toolbar`. It allows you to get information about your +application while you develop. -In a browser, visit `http://localhost:6543/ <http://localhost:6543>`_. -You will see the generated application's default page. -Decisions the ``pyramid_zodb`` Scaffold Has Made For You -======================================================== +Decisions the ``zodb`` scaffold has made for you +------------------------------------------------ -Creating a project using the ``pyramid_zodb`` scaffold makes the following +Creating a project using the ``zodb`` scaffold makes the following assumptions: -- you are willing to use :term:`ZODB` as persistent storage - -- you are willing to use :term:`traversal` to map URLs to code. +- You are willing to use :term:`ZODB` as persistent storage. -- you want to use imperative code plus a :term:`scan` to perform - configuration. +- You are willing to use :term:`traversal` to map URLs to code. .. note:: - :app:`Pyramid` supports any persistent storage mechanism (e.g. a SQL - database or filesystem files, etc). :app:`Pyramid` also supports an - additional mechanism to map URLs to code (:term:`URL dispatch`). However, - for the purposes of this tutorial, we'll only be using traversal and ZODB. - + :app:`Pyramid` supports any persistent storage mechanism (e.g., a SQL + database or filesystem files). It also supports an additional + mechanism to map URLs to code (:term:`URL dispatch`). However, for the + purposes of this tutorial, we'll only be using traversal and ZODB. diff --git a/docs/tutorials/wiki/src/authorization/CHANGES.txt b/docs/tutorials/wiki/src/authorization/CHANGES.txt index e14f633ab..35a34f332 100644 --- a/docs/tutorials/wiki/src/authorization/CHANGES.txt +++ b/docs/tutorials/wiki/src/authorization/CHANGES.txt @@ -1,5 +1,4 @@ 0.0 --- -- Initial version - +- Initial version diff --git a/docs/tutorials/wiki/src/authorization/README.txt b/docs/tutorials/wiki/src/authorization/README.txt index d41f7f90f..dcb3605b8 100644 --- a/docs/tutorials/wiki/src/authorization/README.txt +++ b/docs/tutorials/wiki/src/authorization/README.txt @@ -1,4 +1,12 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini index 1ba746d0e..6bf4b198e 100644 --- a/docs/tutorials/wiki/src/authorization/development.ini +++ b/docs/tutorials/wiki/src/authorization/development.ini @@ -1,34 +1,44 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 - -[pipeline:main] -pipeline = - egg:WebError#evalerror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -40,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -47,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/authorization/production.ini b/docs/tutorials/wiki/src/authorization/production.ini index 5c47ade9b..4e9892e7b 100644 --- a/docs/tutorials/wiki/src/authorization/production.ini +++ b/docs/tutorials/wiki/src/authorization/production.ini @@ -1,45 +1,36 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[pipeline:main] -pipeline = - weberror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial @@ -66,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/authorization/setup.cfg b/docs/tutorials/wiki/src/authorization/setup.cfg deleted file mode 100644 index 3d7ea6e23..000000000 --- a/docs/tutorials/wiki/src/authorization/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true - diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index adfa70c9f..beeed75c9 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -3,30 +3,39 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', - 'repoze.zodbconn', - 'repoze.tm2>=1.0b1', # default_commit_veto - 'repoze.retry', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', 'ZODB3', - 'WebError', + 'waitress', 'docutils', ] +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Intended Audience :: Developers", - "Framework :: Pylons", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -34,13 +43,12 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, + extras_require={ + 'testing': tests_require, + }, install_requires=requires, - tests_require=requires, - test_suite="tutorial", - entry_points = """\ + entry_points="""\ [paste.app_factory] main = tutorial:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py index f7dab5f47..39b94abd1 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -1,31 +1,27 @@ -from repoze.zodbconn.finder import PersistentApplicationFinder - from pyramid.config import Configurator +from pyramid_zodbconn import get_connection + from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from tutorial.models import appmaker -from tutorial.security import groupfinder +from .models import appmaker +from .security import groupfinder + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + def main(global_config, **settings): - """ This function returns a WSGI application. - - It is usually called by the PasteDeploy framework during - ``paster serve``. + """ This function returns a Pyramid WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy(secret='sosecret', - callback=groupfinder) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() - zodb_uri = settings.get('zodb_uri', False) - if zodb_uri is False: - raise ValueError("No 'zodb_uri' in application configuration.") - - finder = PersistentApplicationFinder(zodb_uri, appmaker) - def get_root(request): - return finder(request.environ) - config = Configurator(root_factory=get_root, settings=settings, - authentication_policy=authn_policy, - authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static') - config.scan('tutorial') + config = Configurator(root_factory=root_factory, settings=settings) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py deleted file mode 100644 index 463db71a6..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ /dev/null @@ -1,45 +0,0 @@ -from pyramid.httpexceptions import HTTPFound - -from pyramid.security import remember -from pyramid.security import forget -from pyramid.view import view_config -from pyramid.url import resource_url - -from tutorial.security import USERS - -@view_config(context='tutorial.models.Wiki', name='login', - renderer='templates/login.pt') -@view_config(context='pyramid.exceptions.Forbidden', - renderer='templates/login.pt') -def login(request): - login_url = resource_url(request.context, request, 'login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(context='tutorial.models.Wiki', name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = resource_url(request.context, request), - headers = headers) - diff --git a/docs/tutorials/wiki/src/authorization/tutorial/models.py b/docs/tutorials/wiki/src/authorization/tutorial/models.py index 0a31c38be..38fdd2dfc 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/models.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/models.py @@ -1,8 +1,10 @@ from persistent import Persistent from persistent.mapping import PersistentMapping -from pyramid.security import Allow -from pyramid.security import Everyone +from pyramid.security import ( + Allow, + Everyone, + ) class Wiki(PersistentMapping): __name__ = None @@ -15,7 +17,7 @@ class Page(Persistent): self.data = data def appmaker(zodb_root): - if not 'app_root' in zodb_root: + if 'app_root' not in zodb_root: app_root = Wiki() frontpage = Page('This is the front page') app_root['FrontPage'] = frontpage diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py index cfd13071e..d88c9c71f 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py @@ -5,4 +5,3 @@ GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): if userid in USERS: return GROUPS.get(userid, []) - diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css b/docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css b/docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki/src/authorization/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/theme.css b/docs/tutorials/wiki/src/authorization/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/authorization/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki/src/authorization/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt index f9da6c414..823fa8972 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/edit.pt @@ -1,62 +1,72 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.__name__} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p tal:condition="logged_in" class="pull-right"> + <a href="${request.application_url}/logout">Logout</a> + </p> + <p> + Editing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + <form action="${save_url}" method="post"> + <div class="form-group"> + <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea> + </div> + <div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> + </div> + </form> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Editing <b><span tal:replace="page.__name__">Page Name - Goes Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"> - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - <div id="bottom"> - <div class="bottom"> - <form action="${save_url}" method="post"> - <textarea name="body" tal:content="page.data" rows="10" - cols="60"/><br/> - <input type="submit" name="form.submitted" value="Save"/> - </form> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt index 64e592ea9..4a938e9bb 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/login.pt @@ -1,58 +1,74 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>Login - Pyramid tutorial wiki (based on TurboGears - 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>Login - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p> + <strong> + Login + </strong><br> + <span tal:replace="message"></span> + </p> + <form action="${url}" method="post"> + <input type="hidden" name="came_from" value="${came_from}"> + <div class="form-group"> + <label for="login">Username</label> + <input type="text" name="login" value="${login}"> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" name="password" value="${password}"> + </div> + <div class="form-group"> + <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button> + </div> + </form> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - <b>Login</b><br/> - <span tal:replace="message"/> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <form action="${url}" method="post"> - <input type="hidden" name="came_from" value="${came_from}"/> - <input type="text" name="login" value="${login}"/><br/> - <input type="password" name="password" - value="${password}"/><br/> - <input type="submit" name="form.submitted" value="Log In"/> - </form> </div> </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt index d98420680..f8cbe2e2c 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt @@ -1,75 +1,67 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt index d207a0c23..fa35d758d 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/view.pt @@ -1,65 +1,72 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.__name__} - Pyramid tutorial wiki (based on +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p tal:condition="logged_in" class="pull-right"> + <a href="${request.application_url}/logout">Logout</a> + </p> + <div tal:replace="structure content"> + Page text goes here. + </div> + <p> + <a tal:attributes="href edit_url" href=""> + Edit this page + </a> + </p> + <p> + Viewing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Viewing <b><span tal:replace="page.__name__">Page Name - Goes Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"> - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> - </div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div tal:replace="structure content"> - Page text goes here. + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> - <p> - <a tal:attributes="href edit_url" href=""> - Edit this page - </a> - </p> </div> </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/authorization/tutorial/tests.py b/docs/tutorials/wiki/src/authorization/tutorial/tests.py index aaf753816..40f3c47af 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/tests.py @@ -2,123 +2,16 @@ import unittest from pyramid import testing -class PageModelTests(unittest.TestCase): - def _getTargetClass(self): - from tutorial.models import Page - return Page +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() - def _makeOne(self, data=u'some data'): - return self._getTargetClass()(data=data) + def tearDown(self): + testing.tearDown() - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.data, u'some data') - -class WikiModelTests(unittest.TestCase): - - def _getTargetClass(self): - from tutorial.models import Wiki - return Wiki - - def _makeOne(self): - return self._getTargetClass()() - - def test_it(self): - wiki = self._makeOne() - self.assertEqual(wiki.__parent__, None) - self.assertEqual(wiki.__name__, None) - -class AppmakerTests(unittest.TestCase): - def _callFUT(self, zodb_root): - from tutorial.models import appmaker - return appmaker(zodb_root) - - def test_it(self): - root = {} - self._callFUT(root) - self.assertEqual(root['app_root']['FrontPage'].data, - 'This is the front page') - -class ViewWikiTests(unittest.TestCase): - def test_it(self): - from tutorial.views import view_wiki - context = testing.DummyResource() - request = testing.DummyRequest() - response = view_wiki(context, request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - -class ViewPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import view_page - return view_page(context, request) - - def test_it(self): - wiki = testing.DummyResource() - wiki['IDoExist'] = testing.DummyResource() - context = testing.DummyResource(data='Hello CruelWorld IDoExist') - context.__parent__ = wiki - context.__name__ = 'thepage' - request = testing.DummyRequest() - info = self._callFUT(context, request) - self.assertEqual(info['page'], context) - self.assertEqual( - info['content'], - '<div class="document">\n' - '<p>Hello <a href="http://example.com/add_page/CruelWorld">' - 'CruelWorld</a> ' - '<a href="http://example.com/IDoExist/">' - 'IDoExist</a>' - '</p>\n</div>\n') - self.assertEqual(info['edit_url'], - 'http://example.com/thepage/edit_page') - - -class AddPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import add_page - return add_page(context, request) - - def test_it_notsubmitted(self): - from pyramid.url import resource_url - context = testing.DummyResource() - request = testing.DummyRequest() - request.subpath = ['AnotherPage'] - info = self._callFUT(context, request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - resource_url( - context, request, 'add_page', 'AnotherPage')) - - def test_it_submitted(self): - context = testing.DummyResource() - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.subpath = ['AnotherPage'] - self._callFUT(context, request) - page = context['AnotherPage'] - self.assertEqual(page.data, 'Hello yo!') - self.assertEqual(page.__name__, 'AnotherPage') - self.assertEqual(page.__parent__, context) - -class EditPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import edit_page - return edit_page(context, request) - - def test_it_notsubmitted(self): - from pyramid.url import resource_url - context = testing.DummyResource() + def test_my_view(self): + from .views import my_view request = testing.DummyRequest() - info = self._callFUT(context, request) - self.assertEqual(info['page'], context) - self.assertEqual(info['save_url'], - resource_url(context, request, 'edit_page')) - - def test_it_submitted(self): - context = testing.DummyResource() - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - response = self._callFUT(context, request) - self.assertEqual(response.location, 'http://example.com/') - self.assertEqual(context.data, 'Hello yo!') + info = my_view(request) + self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index a83e17de4..c271d2cc1 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -2,21 +2,31 @@ from docutils.core import publish_parts import re from pyramid.httpexceptions import HTTPFound -from pyramid.url import resource_url -from pyramid.view import view_config -from pyramid.security import authenticated_userid -from tutorial.models import Page +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + + +from .security import USERS +from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='tutorial.models.Wiki', permission='view') +@view_config(context='.models.Wiki', + permission='view') def view_wiki(context, request): - return HTTPFound(location=resource_url(context, request, 'FrontPage')) + return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='tutorial.models.Page', - renderer='templates/view.pt', permission='view') +@view_config(context='.models.Page', renderer='templates/view.pt', + permission='view') def view_page(context, request): wiki = context.__parent__ @@ -24,53 +34,83 @@ def view_page(context, request): word = match.group(1) if word in wiki: page = wiki[word] - view_url = resource_url(page, request) + view_url = request.resource_url(page) return '<a href="%s">%s</a>' % (view_url, word) else: - add_url = request.application_url + '/add_page/' + word + add_url = request.application_url + '/add_page/' + word return '<a href="%s">%s</a>' % (add_url, word) content = publish_parts(context.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) - edit_url = resource_url(context, request, 'edit_page') + edit_url = request.resource_url(context, 'edit_page') - logged_in = authenticated_userid(request) + return dict(page=context, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) - return dict(page = context, content = content, edit_url = edit_url, - logged_in = logged_in) - -@view_config(name='add_page', context='tutorial.models.Wiki', +@view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', permission='edit') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page - return HTTPFound(location = resource_url(page, request)) - save_url = resource_url(context, request, 'add_page', name) + context[pagename] = page + return HTTPFound(location=request.resource_url(page)) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - logged_in = authenticated_userid(request) - - return dict(page = page, save_url = save_url, logged_in = logged_in) + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) -@view_config(name='edit_page', context='tutorial.models.Page', +@view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', permission='edit') def edit_page(context, request): if 'form.submitted' in request.params: context.data = request.params['body'] - return HTTPFound(location = resource_url(context, request)) + return HTTPFound(location=request.resource_url(context)) + + return dict(page=context, + save_url=request.resource_url(context, 'edit_page'), + logged_in=request.authenticated_userid) + +@view_config(context='.models.Wiki', name='login', + renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') +def login(request): + login_url = request.resource_url(request.context, 'login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) - logged_in = authenticated_userid(request) - return dict(page = context, - save_url = resource_url(context, request, 'edit_page'), - logged_in = logged_in) - +@view_config(context='.models.Wiki', name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location=request.resource_url(request.context), + headers=headers) diff --git a/docs/tutorials/wiki/src/basiclayout/README.txt b/docs/tutorials/wiki/src/basiclayout/README.txt index d41f7f90f..dcb3605b8 100644 --- a/docs/tutorials/wiki/src/basiclayout/README.txt +++ b/docs/tutorials/wiki/src/basiclayout/README.txt @@ -1,4 +1,12 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki/src/basiclayout/development.ini b/docs/tutorials/wiki/src/basiclayout/development.ini index 555010bed..6bf4b198e 100644 --- a/docs/tutorials/wiki/src/basiclayout/development.ini +++ b/docs/tutorials/wiki/src/basiclayout/development.ini @@ -1,34 +1,44 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 - -[pipeline:main] -pipeline = - egg:WebError#evalerror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -40,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -47,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/basiclayout/production.ini b/docs/tutorials/wiki/src/basiclayout/production.ini index 5c47ade9b..4e9892e7b 100644 --- a/docs/tutorials/wiki/src/basiclayout/production.ini +++ b/docs/tutorials/wiki/src/basiclayout/production.ini @@ -1,45 +1,36 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[pipeline:main] -pipeline = - weberror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial @@ -66,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/basiclayout/setup.cfg b/docs/tutorials/wiki/src/basiclayout/setup.cfg deleted file mode 100644 index 23b2ad983..000000000 --- a/docs/tutorials/wiki/src/basiclayout/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py index 2d540d65b..46b395568 100644 --- a/docs/tutorials/wiki/src/basiclayout/setup.py +++ b/docs/tutorials/wiki/src/basiclayout/setup.py @@ -3,28 +3,38 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', - 'repoze.zodbconn', - 'repoze.tm2>=1.0b1', # default_commit_veto - 'repoze.retry', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', 'ZODB3', - 'WebError', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -32,13 +42,12 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires = requires, - tests_require= requires, - test_suite="tutorial", - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py index 6a4093a3b..f2a86df47 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py @@ -1,18 +1,18 @@ from pyramid.config import Configurator -from repoze.zodbconn.finder import PersistentApplicationFinder -from tutorial.models import appmaker +from pyramid_zodbconn import get_connection +from .models import appmaker + + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - zodb_uri = settings.get('zodb_uri', False) - if zodb_uri is False: - raise ValueError("No 'zodb_uri' in application configuration.") - - finder = PersistentApplicationFinder(zodb_uri, appmaker) - def get_root(request): - return finder(request.environ) - config = Configurator(root_factory=get_root, settings=settings) - config.add_static_view('static', 'tutorial:static') - config.scan('tutorial') + config = Configurator(root_factory=root_factory, settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py index 8dd0f5a49..e5aa3e9f7 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/models.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/models.py @@ -1,10 +1,12 @@ from persistent.mapping import PersistentMapping + class MyModel(PersistentMapping): __parent__ = __name__ = None + def appmaker(zodb_root): - if not 'app_root' in zodb_root: + if 'app_root' not in zodb_root: app_root = MyModel() zodb_root['app_root'] = app_root import transaction diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt index c24daa711..f8cbe2e2c 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt @@ -1,75 +1,67 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org/">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py index 1f3c3bb4d..40f3c47af 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/tests.py @@ -2,6 +2,7 @@ import unittest from pyramid import testing + class ViewTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -10,8 +11,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'tutorial') - diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py index 157b9ac8f..628ce15ed 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/views.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/views.py @@ -1,7 +1,7 @@ from pyramid.view import view_config -from tutorial.models import MyModel +from .models import MyModel -@view_config(context=MyModel, - renderer='tutorial:templates/mytemplate.pt') + +@view_config(context=MyModel, renderer='templates/mytemplate.pt') def my_view(request): - return {'project':'tutorial'} + return {'project': 'tutorial'} diff --git a/docs/tutorials/wiki/src/installation/CHANGES.txt b/docs/tutorials/wiki/src/installation/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki/src/installation/MANIFEST.in b/docs/tutorials/wiki/src/installation/MANIFEST.in new file mode 100644 index 000000000..81beba1b1 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki/src/installation/README.txt b/docs/tutorials/wiki/src/installation/README.txt new file mode 100644 index 000000000..dcb3605b8 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/README.txt @@ -0,0 +1,12 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki/src/installation/development.ini b/docs/tutorials/wiki/src/installation/development.ini new file mode 100644 index 000000000..6bf4b198e --- /dev/null +++ b/docs/tutorials/wiki/src/installation/development.ini @@ -0,0 +1,65 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/installation/production.ini b/docs/tutorials/wiki/src/installation/production.ini new file mode 100644 index 000000000..4e9892e7b --- /dev/null +++ b/docs/tutorials/wiki/src/installation/production.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/installation/setup.py b/docs/tutorials/wiki/src/installation/setup.py new file mode 100644 index 000000000..46b395568 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/setup.py @@ -0,0 +1,53 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', + 'ZODB3', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, + ) diff --git a/docs/tutorials/wiki/src/installation/tutorial/__init__.py b/docs/tutorials/wiki/src/installation/tutorial/__init__.py new file mode 100644 index 000000000..f2a86df47 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/__init__.py @@ -0,0 +1,18 @@ +from pyramid.config import Configurator +from pyramid_zodbconn import get_connection +from .models import appmaker + + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(root_factory=root_factory, settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/installation/tutorial/models.py b/docs/tutorials/wiki/src/installation/tutorial/models.py new file mode 100644 index 000000000..e5aa3e9f7 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/models.py @@ -0,0 +1,14 @@ +from persistent.mapping import PersistentMapping + + +class MyModel(PersistentMapping): + __parent__ = __name__ = None + + +def appmaker(zodb_root): + if 'app_root' not in zodb_root: + app_root = MyModel() + zodb_root['app_root'] = app_root + import transaction + transaction.commit() + return zodb_root['app_root'] diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/installation/tutorial/static/theme.css b/docs/tutorials/wiki/src/installation/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt new file mode 100644 index 000000000..f8cbe2e2c --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/templates/mytemplate.pt @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki/src/installation/tutorial/tests.py b/docs/tutorials/wiki/src/installation/tutorial/tests.py new file mode 100644 index 000000000..40f3c47af --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/tests.py @@ -0,0 +1,17 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/installation/tutorial/views.py b/docs/tutorials/wiki/src/installation/tutorial/views.py new file mode 100644 index 000000000..628ce15ed --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tutorial/views.py @@ -0,0 +1,7 @@ +from pyramid.view import view_config +from .models import MyModel + + +@view_config(context=MyModel, renderer='templates/mytemplate.pt') +def my_view(request): + return {'project': 'tutorial'} diff --git a/docs/tutorials/wiki/src/models/CHANGES.txt b/docs/tutorials/wiki/src/models/CHANGES.txt index ffa255da8..35a34f332 100644 --- a/docs/tutorials/wiki/src/models/CHANGES.txt +++ b/docs/tutorials/wiki/src/models/CHANGES.txt @@ -1,4 +1,4 @@ 0.0 --- -- Initial version +- Initial version diff --git a/docs/tutorials/wiki/src/models/README.txt b/docs/tutorials/wiki/src/models/README.txt index d41f7f90f..dcb3605b8 100644 --- a/docs/tutorials/wiki/src/models/README.txt +++ b/docs/tutorials/wiki/src/models/README.txt @@ -1,4 +1,12 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki/src/models/development.ini b/docs/tutorials/wiki/src/models/development.ini index 1ba746d0e..6bf4b198e 100644 --- a/docs/tutorials/wiki/src/models/development.ini +++ b/docs/tutorials/wiki/src/models/development.ini @@ -1,34 +1,44 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 - -[pipeline:main] -pipeline = - egg:WebError#evalerror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -40,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -47,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/models/production.ini b/docs/tutorials/wiki/src/models/production.ini index 5c47ade9b..4e9892e7b 100644 --- a/docs/tutorials/wiki/src/models/production.ini +++ b/docs/tutorials/wiki/src/models/production.ini @@ -1,45 +1,36 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[pipeline:main] -pipeline = - weberror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial @@ -66,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/models/setup.cfg b/docs/tutorials/wiki/src/models/setup.cfg deleted file mode 100644 index 3d7ea6e23..000000000 --- a/docs/tutorials/wiki/src/models/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true - diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py index 2d540d65b..46b395568 100644 --- a/docs/tutorials/wiki/src/models/setup.py +++ b/docs/tutorials/wiki/src/models/setup.py @@ -3,28 +3,38 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', - 'repoze.zodbconn', - 'repoze.tm2>=1.0b1', # default_commit_veto - 'repoze.retry', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', 'ZODB3', - 'WebError', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -32,13 +42,12 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires = requires, - tests_require= requires, - test_suite="tutorial", - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki/src/models/tutorial/__init__.py b/docs/tutorials/wiki/src/models/tutorial/__init__.py index 73fc81d23..f2a86df47 100644 --- a/docs/tutorials/wiki/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/models/tutorial/__init__.py @@ -1,19 +1,18 @@ from pyramid.config import Configurator -from repoze.zodbconn.finder import PersistentApplicationFinder -from tutorial.models import appmaker +from pyramid_zodbconn import get_connection +from .models import appmaker + + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ - zodb_uri = settings.get('zodb_uri', False) - if zodb_uri is False: - raise ValueError("No 'zodb_uri' in application configuration.") - - finder = PersistentApplicationFinder(zodb_uri, appmaker) - def get_root(request): - return finder(request.environ) - config = Configurator(root_factory=get_root, settings=settings) - config.add_static_view('static', 'tutorial:static') - config.scan('tutorial') + config = Configurator(root_factory=root_factory, settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() - diff --git a/docs/tutorials/wiki/src/models/tutorial/models.py b/docs/tutorials/wiki/src/models/tutorial/models.py index 9761856c6..aa907aee5 100644 --- a/docs/tutorials/wiki/src/models/tutorial/models.py +++ b/docs/tutorials/wiki/src/models/tutorial/models.py @@ -10,7 +10,7 @@ class Page(Persistent): self.data = data def appmaker(zodb_root): - if not 'app_root' in zodb_root: + if 'app_root' not in zodb_root: app_root = Wiki() frontpage = Page('This is the front page') app_root['FrontPage'] = frontpage diff --git a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/static/ie6.css b/docs/tutorials/wiki/src/models/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pylons.css b/docs/tutorials/wiki/src/models/tutorial/static/pylons.css deleted file mode 100644 index a9f49cc85..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-snall,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/models/tutorial/static/theme.css b/docs/tutorials/wiki/src/models/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt index d98420680..f8cbe2e2c 100644 --- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt @@ -1,75 +1,67 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/models/tutorial/tests.py b/docs/tutorials/wiki/src/models/tutorial/tests.py index 51c97a95d..40f3c47af 100644 --- a/docs/tutorials/wiki/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki/src/models/tutorial/tests.py @@ -2,50 +2,6 @@ import unittest from pyramid import testing -class PageModelTests(unittest.TestCase): - - def _getTargetClass(self): - from tutorial.models import Page - return Page - - def _makeOne(self, data=u'some data'): - return self._getTargetClass()(data=data) - - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.data, u'some data') - -class WikiModelTests(unittest.TestCase): - - def _getTargetClass(self): - from tutorial.models import Wiki - return Wiki - - def _makeOne(self): - return self._getTargetClass()() - - def test_it(self): - wiki = self._makeOne() - self.assertEqual(wiki.__parent__, None) - self.assertEqual(wiki.__name__, None) - -class AppmakerTests(unittest.TestCase): - - def _callFUT(self, zodb_root): - from tutorial.models import appmaker - return appmaker(zodb_root) - - def test_no_app_root(self): - root = {} - self._callFUT(root) - self.assertEqual(root['app_root']['FrontPage'].data, - 'This is the front page') - - def test_w_app_root(self): - app_root = object() - root = {'app_root': app_root} - self._callFUT(root) - self.failUnless(root['app_root'] is app_root) class ViewTests(unittest.TestCase): def setUp(self): @@ -55,7 +11,7 @@ class ViewTests(unittest.TestCase): testing.tearDown() def test_my_view(self): - from tutorial.views import my_view + from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/models/tutorial/views.py b/docs/tutorials/wiki/src/models/tutorial/views.py index 2346602c9..628ce15ed 100644 --- a/docs/tutorials/wiki/src/models/tutorial/views.py +++ b/docs/tutorials/wiki/src/models/tutorial/views.py @@ -1,5 +1,7 @@ from pyramid.view import view_config +from .models import MyModel -@view_config(renderer='tutorial:templates/mytemplate.pt') + +@view_config(context=MyModel, renderer='templates/mytemplate.pt') def my_view(request): - return {'project':'tutorial'} + return {'project': 'tutorial'} diff --git a/docs/tutorials/wiki/src/tests/CHANGES.txt b/docs/tutorials/wiki/src/tests/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki/src/tests/MANIFEST.in b/docs/tutorials/wiki/src/tests/MANIFEST.in new file mode 100644 index 000000000..81beba1b1 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki/src/tests/README.txt b/docs/tutorials/wiki/src/tests/README.txt new file mode 100644 index 000000000..dcb3605b8 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/README.txt @@ -0,0 +1,12 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki/src/tests/development.ini b/docs/tutorials/wiki/src/tests/development.ini new file mode 100644 index 000000000..6bf4b198e --- /dev/null +++ b/docs/tutorials/wiki/src/tests/development.ini @@ -0,0 +1,65 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/tests/production.ini b/docs/tutorials/wiki/src/tests/production.ini new file mode 100644 index 000000000..4e9892e7b --- /dev/null +++ b/docs/tutorials/wiki/src/tests/production.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py new file mode 100644 index 000000000..beeed75c9 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -0,0 +1,54 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', + 'ZODB3', + 'waitress', + 'docutils', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, + ) diff --git a/docs/tutorials/wiki/src/tests/tutorial/__init__.py b/docs/tutorials/wiki/src/tests/tutorial/__init__.py new file mode 100644 index 000000000..39b94abd1 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py @@ -0,0 +1,27 @@ +from pyramid.config import Configurator +from pyramid_zodbconn import get_connection + +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from .models import appmaker +from .security import groupfinder + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config = Configurator(root_factory=root_factory, settings=settings) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/tests/tutorial/models.py b/docs/tutorials/wiki/src/tests/tutorial/models.py new file mode 100644 index 000000000..38fdd2dfc --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/models.py @@ -0,0 +1,29 @@ +from persistent import Persistent +from persistent.mapping import PersistentMapping + +from pyramid.security import ( + Allow, + Everyone, + ) + +class Wiki(PersistentMapping): + __name__ = None + __parent__ = None + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + +class Page(Persistent): + def __init__(self, data): + self.data = data + +def appmaker(zodb_root): + if 'app_root' not in zodb_root: + app_root = Wiki() + frontpage = Page('This is the front page') + app_root['FrontPage'] = frontpage + frontpage.__name__ = 'FrontPage' + frontpage.__parent__ = app_root + zodb_root['app_root'] = app_root + import transaction + transaction.commit() + return zodb_root['app_root'] diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py new file mode 100644 index 000000000..d88c9c71f --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/security.py @@ -0,0 +1,7 @@ +USERS = {'editor':'editor', + 'viewer':'viewer'} +GROUPS = {'editor':['group:editors']} + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/tests/tutorial/static/theme.css b/docs/tutorials/wiki/src/tests/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt new file mode 100644 index 000000000..823fa8972 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/edit.pt @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p tal:condition="logged_in" class="pull-right"> + <a href="${request.application_url}/logout">Logout</a> + </p> + <p> + Editing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + <form action="${save_url}" method="post"> + <div class="form-group"> + <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea> + </div> + <div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> + </div> + </form> + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt new file mode 100644 index 000000000..4a938e9bb --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/login.pt @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>Login - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p> + <strong> + Login + </strong><br> + <span tal:replace="message"></span> + </p> + <form action="${url}" method="post"> + <input type="hidden" name="came_from" value="${came_from}"> + <div class="form-group"> + <label for="login">Username</label> + <input type="text" name="login" value="${login}"> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" name="password" value="${password}"> + </div> + <div class="form-group"> + <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button> + </div> + </form> + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt new file mode 100644 index 000000000..f8cbe2e2c --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt new file mode 100644 index 000000000..fa35d758d --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/view.pt @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p tal:condition="logged_in" class="pull-right"> + <a href="${request.application_url}/logout">Logout</a> + </p> + <div tal:replace="structure content"> + Page text goes here. + </div> + <p> + <a tal:attributes="href edit_url" href=""> + Edit this page + </a> + </p> + <p> + Viewing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index 0ce5ea718..04beaea44 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -5,7 +5,7 @@ from pyramid import testing class PageModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Page + from .models import Page return Page def _makeOne(self, data=u'some data'): @@ -18,7 +18,7 @@ class PageModelTests(unittest.TestCase): class WikiModelTests(unittest.TestCase): def _getTargetClass(self): - from tutorial.models import Wiki + from .models import Wiki return Wiki def _makeOne(self): @@ -30,8 +30,9 @@ class WikiModelTests(unittest.TestCase): self.assertEqual(wiki.__name__, None) class AppmakerTests(unittest.TestCase): + def _callFUT(self, zodb_root): - from tutorial.models import appmaker + from .models import appmaker return appmaker(zodb_root) def test_it(self): @@ -42,7 +43,7 @@ class AppmakerTests(unittest.TestCase): class ViewWikiTests(unittest.TestCase): def test_it(self): - from tutorial.views import view_wiki + from .views import view_wiki context = testing.DummyResource() request = testing.DummyRequest() response = view_wiki(context, request) @@ -50,7 +51,7 @@ class ViewWikiTests(unittest.TestCase): class ViewPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import view_page + from .views import view_page return view_page(context, request) def test_it(self): @@ -76,11 +77,10 @@ class ViewPageTests(unittest.TestCase): class AddPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import add_page + from .views import add_page return add_page(context, request) def test_it_notsubmitted(self): - from pyramid.url import resource_url context = testing.DummyResource() request = testing.DummyRequest() request.subpath = ['AnotherPage'] @@ -88,7 +88,7 @@ class AddPageTests(unittest.TestCase): self.assertEqual(info['page'].data,'') self.assertEqual( info['save_url'], - resource_url(context, request, 'add_page', 'AnotherPage')) + request.resource_url(context, 'add_page', 'AnotherPage')) def test_it_submitted(self): context = testing.DummyResource() @@ -103,17 +103,16 @@ class AddPageTests(unittest.TestCase): class EditPageTests(unittest.TestCase): def _callFUT(self, context, request): - from tutorial.views import edit_page + from .views import edit_page return edit_page(context, request) def test_it_notsubmitted(self): - from pyramid.url import resource_url context = testing.DummyResource() request = testing.DummyRequest() info = self._callFUT(context, request) self.assertEqual(info['page'], context) self.assertEqual(info['save_url'], - resource_url(context, request, 'edit_page')) + request.resource_url(context, 'edit_page')) def test_it_submitted(self): context = testing.DummyResource() @@ -135,18 +134,16 @@ class FunctionalTests(unittest.TestCase): def setUp(self): import tempfile import os.path - from tutorial import main + from . import main self.tmpdir = tempfile.mkdtemp() dbpath = os.path.join( self.tmpdir, 'test.db') - from repoze.zodbconn.uri import db_from_uri - db = db_from_uri('file://' + dbpath) - settings = { 'zodb_uri' : None } + uri = 'file://' + dbpath + settings = { 'zodbconn.uri' : uri , + 'pyramid.includes': ['pyramid_zodbconn', 'pyramid_tm'] } app = main({}, **settings) - from repoze.zodbconn.connector import Connector - app = Connector(app, db) - self.db = db + self.db = app.registry._zodb_databases[''] from webtest import TestApp self.testapp = TestApp(app) @@ -157,64 +154,68 @@ class FunctionalTests(unittest.TestCase): def test_root(self): res = self.testapp.get('/', status=302) - self.assertTrue(not res.body) + self.assertEqual(res.location, 'http://localhost/FrontPage') def test_FrontPage(self): res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) + self.assertTrue(b'FrontPage' in res.body) def test_unexisting_page(self): res = self.testapp.get('/SomePage', status=404) - self.assertTrue('Not Found' in res.body) + self.assertTrue(b'Not Found' in res.body) + + def test_referrer_is_login(self): + res = self.testapp.get('/login', status=200) + self.assertTrue(b'name="came_from" value="/"' in res.body) def test_successful_log_in(self): res = self.testapp.get( self.viewer_login, status=302) - self.assertTrue(res.location == 'FrontPage') + self.assertEqual(res.location, 'http://localhost/FrontPage') def test_failed_log_in(self): res = self.testapp.get( self.viewer_wrong_login, status=200) - self.assertTrue('login' in res.body) + self.assertTrue(b'login' in res.body) def test_logout_link_present_when_logged_in(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('Logout' in res.body) + self.assertTrue(b'Logout' in res.body) def test_logout_link_not_present_after_logged_out(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage', status=200) res = self.testapp.get('/logout', status=302) - self.assertTrue('Logout' not in res.body) + self.assertTrue(b'Logout' not in res.body) def test_anonymous_user_cannot_edit(self): res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_anonymous_user_cannot_add(self): res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_edit(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_add(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_editors_member_user_can_edit(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Editing' in res.body) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_add(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Editing' in res.body) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_view(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py new file mode 100644 index 000000000..c271d2cc1 --- /dev/null +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -0,0 +1,116 @@ +from docutils.core import publish_parts +import re + +from pyramid.httpexceptions import HTTPFound + +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + + +from .security import USERS +from .models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(context='.models.Wiki', + permission='view') +def view_wiki(context, request): + return HTTPFound(location=request.resource_url(context, 'FrontPage')) + +@view_config(context='.models.Page', renderer='templates/view.pt', + permission='view') +def view_page(context, request): + wiki = context.__parent__ + + def check(match): + word = match.group(1) + if word in wiki: + page = wiki[word] + view_url = request.resource_url(page) + return '<a href="%s">%s</a>' % (view_url, word) + else: + add_url = request.application_url + '/add_page/' + word + return '<a href="%s">%s</a>' % (add_url, word) + + content = publish_parts(context.data, writer_name='html')['html_body'] + content = wikiwords.sub(check, content) + edit_url = request.resource_url(context, 'edit_page') + + return dict(page=context, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) + +@view_config(name='add_page', context='.models.Wiki', + renderer='templates/edit.pt', + permission='edit') +def add_page(context, request): + pagename = request.subpath[0] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(body) + page.__name__ = pagename + page.__parent__ = context + context[pagename] = page + return HTTPFound(location=request.resource_url(page)) + save_url = request.resource_url(context, 'add_page', pagename) + page = Page('') + page.__name__ = pagename + page.__parent__ = context + + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) + +@view_config(name='edit_page', context='.models.Page', + renderer='templates/edit.pt', + permission='edit') +def edit_page(context, request): + if 'form.submitted' in request.params: + context.data = request.params['body'] + return HTTPFound(location=request.resource_url(context)) + + return dict(page=context, + save_url=request.resource_url(context, 'edit_page'), + logged_in=request.authenticated_userid) + +@view_config(context='.models.Wiki', name='login', + renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') +def login(request): + login_url = request.resource_url(request.context, 'login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + +@view_config(context='.models.Wiki', name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location=request.resource_url(request.context), + headers=headers) diff --git a/docs/tutorials/wiki/src/views/CHANGES.txt b/docs/tutorials/wiki/src/views/CHANGES.txt index 1544cf53b..35a34f332 100644 --- a/docs/tutorials/wiki/src/views/CHANGES.txt +++ b/docs/tutorials/wiki/src/views/CHANGES.txt @@ -1,3 +1,4 @@ -0.1 +0.0 +--- - Initial version +- Initial version diff --git a/docs/tutorials/wiki/src/views/README.txt b/docs/tutorials/wiki/src/views/README.txt index d41f7f90f..dcb3605b8 100644 --- a/docs/tutorials/wiki/src/views/README.txt +++ b/docs/tutorials/wiki/src/views/README.txt @@ -1,4 +1,12 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki/src/views/development.ini b/docs/tutorials/wiki/src/views/development.ini index 555010bed..6bf4b198e 100644 --- a/docs/tutorials/wiki/src/views/development.ini +++ b/docs/tutorials/wiki/src/views/development.ini @@ -1,34 +1,44 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 - -[pipeline:main] -pipeline = - egg:WebError#evalerror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -40,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -47,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/views/production.ini b/docs/tutorials/wiki/src/views/production.ini index 5c47ade9b..4e9892e7b 100644 --- a/docs/tutorials/wiki/src/views/production.ini +++ b/docs/tutorials/wiki/src/views/production.ini @@ -1,45 +1,36 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 -[pipeline:main] -pipeline = - weberror - egg:repoze.zodbconn#closer - egg:repoze.retry#retry - tm - tutorial +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial @@ -66,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/views/setup.cfg b/docs/tutorials/wiki/src/views/setup.cfg deleted file mode 100644 index 3d7ea6e23..000000000 --- a/docs/tutorials/wiki/src/views/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true - diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py index daa5e5eb1..beeed75c9 100644 --- a/docs/tutorials/wiki/src/views/setup.py +++ b/docs/tutorials/wiki/src/views/setup.py @@ -3,30 +3,39 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', - 'repoze.zodbconn', - 'repoze.tm2>=1.0b1', # default_commit_veto - 'repoze.retry', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', 'ZODB3', - 'WebError', + 'waitress', 'docutils', ] +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Intended Audience :: Developers", - "Framework :: Pylons", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -34,12 +43,12 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, + extras_require={ + 'testing': tests_require, + }, install_requires=requires, - tests_require=requires, - test_suite="tutorial", - entry_points = """\ + entry_points="""\ [paste.app_factory] main = tutorial:main """, - paster_plugins=['pyramid'], ) diff --git a/docs/tutorials/wiki/src/views/tutorial/__init__.py b/docs/tutorials/wiki/src/views/tutorial/__init__.py index 04a01fead..f2a86df47 100644 --- a/docs/tutorials/wiki/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/views/tutorial/__init__.py @@ -1,18 +1,18 @@ from pyramid.config import Configurator -from repoze.zodbconn.finder import PersistentApplicationFinder -from tutorial.models import appmaker +from pyramid_zodbconn import get_connection +from .models import appmaker + + +def root_factory(request): + conn = get_connection(request) + return appmaker(conn.root()) + def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ - zodb_uri = settings.get('zodb_uri', False) - if zodb_uri is False: - raise ValueError("No 'zodb_uri' in application configuration.") - - finder = PersistentApplicationFinder(zodb_uri, appmaker) - def get_root(request): - return finder(request.environ) - config = Configurator(root_factory=get_root, settings=settings) - config.add_static_view('static', 'tutorial:static') - config.scan('tutorial') + config = Configurator(root_factory=root_factory, settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/views/tutorial/models.py b/docs/tutorials/wiki/src/views/tutorial/models.py index 9761856c6..aa907aee5 100644 --- a/docs/tutorials/wiki/src/views/tutorial/models.py +++ b/docs/tutorials/wiki/src/views/tutorial/models.py @@ -10,7 +10,7 @@ class Page(Persistent): self.data = data def appmaker(zodb_root): - if not 'app_root' in zodb_root: + if 'app_root' not in zodb_root: app_root = Wiki() frontpage = Page('This is the front page') app_root['FrontPage'] = frontpage diff --git a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/static/ie6.css b/docs/tutorials/wiki/src/views/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pylons.css b/docs/tutorials/wiki/src/views/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki/src/views/tutorial/static/theme.css b/docs/tutorials/wiki/src/views/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt index 6dbb0edde..b23f45d56 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt @@ -1,58 +1,69 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.__name__} - Pyramid tutorial wiki (based on +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <p> + Editing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + <form action="${save_url}" method="post"> + <div class="form-group"> + <textarea class="form-control" name="body" tal:content="page.data" rows="10" cols="60"></textarea> + </div> + <div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> + </div> + </form> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Editing <b><span tal:replace="page.__name__">Page Name Goes - Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <form action="${save_url}" method="post"> - <textarea name="body" tal:content="page.data" rows="10" - cols="60"/><br/> - <input type="submit" name="form.submitted" value="Save"/> - </form> </div> </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt index d98420680..f8cbe2e2c 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt @@ -1,75 +1,67 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>ZODB Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> + </div> + </div> </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> </div> </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt index 537ae3a15..93580658b 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt @@ -1,61 +1,69 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.__name__} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="${request.static_url('tutorial:static/theme.css')}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="${request.static_url('tutorial:static/pyramid.png')}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + <div tal:replace="structure content"> + Page text goes here. + </div> + <p> + <a tal:attributes="href edit_url" href=""> + Edit this page + </a> + </p> + <p> + Viewing <strong><span tal:replace="page.__name__"> + Page Name Goes Here</span></strong> + </p> + <p>You can return to the + <a href="${request.application_url}">FrontPage</a>. + </p> + </div> + </div> </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Viewing <b><span tal:replace="page.__name__">Page Name Goes - Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div tal:replace="structure content"> - Page text goes here. + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> </div> - <p> - <a tal:attributes="href edit_url" href=""> - Edit this page - </a> - </p> </div> </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> </html> diff --git a/docs/tutorials/wiki/src/views/tutorial/tests.py b/docs/tutorials/wiki/src/views/tutorial/tests.py index 28e424884..40f3c47af 100644 --- a/docs/tutorials/wiki/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki/src/views/tutorial/tests.py @@ -2,126 +2,16 @@ import unittest from pyramid import testing -class PageModelTests(unittest.TestCase): - def _getTargetClass(self): - from tutorial.models import Page - return Page +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() - def _makeOne(self, data=u'some data'): - return self._getTargetClass()(data=data) + def tearDown(self): + testing.tearDown() - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.data, u'some data') - -class WikiModelTests(unittest.TestCase): - - def _getTargetClass(self): - from tutorial.models import Wiki - return Wiki - - def _makeOne(self): - return self._getTargetClass()() - - def test_it(self): - wiki = self._makeOne() - self.assertEqual(wiki.__parent__, None) - self.assertEqual(wiki.__name__, None) - -class AppmakerTests(unittest.TestCase): - def _callFUT(self, zodb_root): - from tutorial.models import appmaker - return appmaker(zodb_root) - - def test_it(self): - root = {} - self._callFUT(root) - self.assertEqual(root['app_root']['FrontPage'].data, - 'This is the front page') - -class ViewWikiTests(unittest.TestCase): - def test_it(self): - from tutorial.views import view_wiki - context = testing.DummyResource() - request = testing.DummyRequest() - response = view_wiki(context, request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - -class ViewPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import view_page - return view_page(context, request) - - def test_it(self): - wiki = testing.DummyResource() - wiki['IDoExist'] = testing.DummyResource() - context = testing.DummyResource(data='Hello CruelWorld IDoExist') - context.__parent__ = wiki - context.__name__ = 'thepage' - request = testing.DummyRequest() - info = self._callFUT(context, request) - self.assertEqual(info['page'], context) - self.assertEqual( - info['content'], - '<div class="document">\n' - '<p>Hello <a href="http://example.com/add_page/CruelWorld">' - 'CruelWorld</a> ' - '<a href="http://example.com/IDoExist/">' - 'IDoExist</a>' - '</p>\n</div>\n') - self.assertEqual(info['edit_url'], - 'http://example.com/thepage/edit_page') - - -class AddPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import add_page - return add_page(context, request) - - def test_it_notsubmitted(self): - from pyramid.url import resource_url - context = testing.DummyResource() - request = testing.DummyRequest() - request.subpath = ['AnotherPage'] - info = self._callFUT(context, request) - self.assertEqual(info['page'].data,'') - self.assertEqual( - info['save_url'], - resource_url(context, request, 'add_page', 'AnotherPage')) - - def test_it_submitted(self): - context = testing.DummyResource() - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.subpath = ['AnotherPage'] - self._callFUT(context, request) - page = context['AnotherPage'] - self.assertEqual(page.data, 'Hello yo!') - self.assertEqual(page.__name__, 'AnotherPage') - self.assertEqual(page.__parent__, context) - -class EditPageTests(unittest.TestCase): - def _callFUT(self, context, request): - from tutorial.views import edit_page - return edit_page(context, request) - - def test_it_notsubmitted(self): - from pyramid.url import resource_url - context = testing.DummyResource() + def test_my_view(self): + from .views import my_view request = testing.DummyRequest() - info = self._callFUT(context, request) - self.assertEqual(info['page'], context) - self.assertEqual(info['save_url'], - resource_url(context, request, 'edit_page')) - - def test_it_submitted(self): - context = testing.DummyResource() - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - response = self._callFUT(context, request) - self.assertEqual(response.location, 'http://example.com/') - self.assertEqual(context.data, 'Hello yo!') - - - + info = my_view(request) + self.assertEqual(info['project'], 'tutorial') diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index 42420f2fe..61517c31d 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -2,20 +2,18 @@ from docutils.core import publish_parts import re from pyramid.httpexceptions import HTTPFound -from pyramid.url import resource_url from pyramid.view import view_config -from tutorial.models import Page +from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='tutorial.models.Wiki') +@view_config(context='.models.Wiki') def view_wiki(context, request): - return HTTPFound(location=resource_url(context, request, 'FrontPage')) + return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='tutorial.models.Page', - renderer='tutorial:templates/view.pt') +@view_config(context='.models.Page', renderer='templates/view.pt') def view_page(context, request): wiki = context.__parent__ @@ -23,7 +21,7 @@ def view_page(context, request): word = match.group(1) if word in wiki: page = wiki[word] - view_url = resource_url(page, request) + view_url = request.resource_url(page) return '<a href="%s">%s</a>' % (view_url, word) else: add_url = request.application_url + '/add_page/' + word @@ -31,34 +29,32 @@ def view_page(context, request): content = publish_parts(context.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) - edit_url = resource_url(context, request, 'edit_page') + edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url) -@view_config(name='add_page', context='tutorial.models.Wiki', - renderer='tutorial:templates/edit.pt') +@view_config(name='add_page', context='.models.Wiki', + renderer='templates/edit.pt') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page - return HTTPFound(location = resource_url(page, request)) - save_url = resource_url(context, request, 'add_page', name) + context[pagename] = page + return HTTPFound(location = request.resource_url(page)) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url) -@view_config(name='edit_page', context='tutorial.models.Page', - renderer='tutorial:templates/edit.pt') +@view_config(name='edit_page', context='.models.Page', + renderer='templates/edit.pt') def edit_page(context, request): if 'form.submitted' in request.params: context.data = request.params['body'] - return HTTPFound(location = resource_url(context, request)) + return HTTPFound(location = request.resource_url(context)) - return dict(page = context, - save_url = resource_url(context, request, 'edit_page')) - - + return dict(page=context, + save_url=request.resource_url(context, 'edit_page')) diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst index c843a0129..788ec595b 100644 --- a/docs/tutorials/wiki/tests.rst +++ b/docs/tutorials/wiki/tests.rst @@ -1,78 +1,75 @@ +.. _wiki_adding_tests: + ============ Adding Tests ============ -We will now add tests for the models and the views and a few functional -tests in the ``tests.py``. Tests ensure that an application works, and -that it continues to work after some changes are made in the future. +We will now add tests for the models and the views and a few functional tests +in ``tests.py``. Tests ensure that an application works, and that it +continues to work when changes are made in the future. -Testing the Models -================== +Test the models +=============== -We write tests for the model -classes and the appmaker. Changing ``tests.py``, we'll write a separate test -class for each model class, and we'll write a test class for the -``appmaker``. +We write tests for the ``model`` classes and the ``appmaker``. Changing +``tests.py``, we'll write a separate test class for each ``model`` class, and +we'll write a test class for the ``appmaker``. -To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_zodb`` project generator. We'll add three test -classes: one for the ``Page`` model named ``PageModelTests``, one for the -``Wiki`` model named ``WikiModelTests``, and one for the appmaker named -``AppmakerTests``. +To do so, we'll retain the ``tutorial.tests.ViewTests`` class that was +generated as part of the ``zodb`` scaffold. We'll add three test classes: one +for the ``Page`` model named ``PageModelTests``, one for the ``Wiki`` model +named ``WikiModelTests``, and one for the appmaker named ``AppmakerTests``. -Testing the Views -================= +Test the views +============== We'll modify our ``tests.py`` file, adding tests for each view function we -added above. As a result, we'll *delete* the ``ViewTests`` test in the file, -and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, -``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, -``view_page``, ``add_page``, and ``edit_page`` views respectively. - +added previously. As a result, we'll *delete* the ``ViewTests`` class that +the ``zodb`` scaffold provided, and add four other test classes: +``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. +These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` +views. Functional tests ================ -We test the whole application, covering security aspects that are not +We'll test the whole application, covering security aspects that are not tested in the unit tests, like logging in, logging out, checking that the ``viewer`` user cannot add or edit pages, but the ``editor`` user can, and so on. -Viewing the results of all our edits to ``tests.py`` -==================================================== +View the results of all our edits to ``tests.py`` +================================================= -Once we're done with the ``tests.py`` module, it will look a lot like the -below: +Open the ``tutorial/tests.py`` module, and edit it such that it appears as +follows: .. literalinclude:: src/tests/tutorial/tests.py :linenos: :language: python -Running the Tests +Running the tests ================= -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. Assuming our shell's current working directory is the -"tutorial" distribution directory: +We can run these tests by using ``py.test`` similarly to how we did in +:ref:`running_tests`. Our testing dependencies have already been satisfied, +courtesy of the scaffold, so we can jump right to running tests. On UNIX: .. code-block:: text - $ ../bin/python setup.py test -q + $ $VENV/bin/py.test tutorial/tests.py -q On Windows: .. code-block:: text - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial/tests.py -q -The expected result looks something like: +The expected result should look like the following: .. code-block:: text - ......... - ---------------------------------------------------------------------- - Ran 23 tests in 1.653s - - OK + ........................ + 24 passed in 2.46 seconds diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst new file mode 100644 index 000000000..5447db861 --- /dev/null +++ b/docs/tutorials/wiki2/authentication.rst @@ -0,0 +1,312 @@ +.. _wiki2_adding_authentication: + +===================== +Adding authentication +===================== + +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. In this section we'll focus solely on the authentication +APIs to add login and logout functionality to our wiki. + +We will implement authentication with the following steps: + +* Add an :term:`authentication policy` and a ``request.user`` computed property + (``security.py``). +* Add routes for ``/login`` and ``/logout`` (``routes.py``). +* Add login and logout views (``views/auth.py``). +* Add a login template (``login.jinja2``). +* Add "Login" and "Logout" links to every page based on the user's + authenticated state (``layout.jinja2``). +* Make the existing views verify user state (``views/default.py``). +* Redirect to ``/login`` when a user is denied access to any of the views that + require permission, instead of a default "403 Forbidden" page + (``views/auth.py``). + + +Authenticating requests +----------------------- + +The core of :app:`Pyramid` authentication is an :term:`authentication policy` +which is used to identify authentication information from a ``request``, +as well as handling the low-level login and logout operations required to +track users across requests (via cookies, headers, or whatever else you can +imagine). + + +Add the authentication policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/security.py`` with the following content: + +.. literalinclude:: src/authentication/tutorial/security.py + :linenos: + :language: python + +Here we've defined: + +* A new authentication policy named ``MyAuthenticationPolicy``, which is + subclassed from Pyramid's + :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the + :term:`userid` using a signed cookie (lines 7-11). +* A ``get_user`` function, which can convert the ``unauthenticated_userid`` + from the policy into a ``User`` object from our database (lines 13-17). +* The ``get_user`` is registered on the request as ``request.user`` to be used + throughout our application as the authenticated ``User`` object for the + logged-in user (line 27). + +The logic in this file is a little bit interesting, so we'll go into detail +about what's happening here: + +First, the default authentication policies all provide a method named +``unauthenticated_userid`` which is responsible for the low-level parsing +of the information in the request (cookies, headers, etc.). If a ``userid`` +is found, then it is returned from this method. This is named +``unauthenticated_userid`` because, at the lowest level, it knows the value of +the userid in the cookie, but it doesn't know if it's actually a user in our +system (remember, anything the user sends to our app is untrusted). + +Second, our application should only care about ``authenticated_userid`` and +``request.user``, which have gone through our application-specific process of +validating that the user is logged in. + +In order to provide an ``authenticated_userid`` we need a verification step. +That can happen anywhere, so we've elected to do it inside of the cached +``request.user`` computed property. This is a convenience that makes +``request.user`` the source of truth in our system. It is either ``None`` or +a ``User`` object from our database. This is why the ``get_user`` function +uses the ``unauthenticated_userid`` to check the database. + + +Configure the app +~~~~~~~~~~~~~~~~~ + +Since we've added a new ``tutorial/security.py`` module, we need to include it. +Open the file ``tutorial/__init__.py`` and edit the following lines: + +.. literalinclude:: src/authentication/tutorial/__init__.py + :linenos: + :emphasize-lines: 11 + :language: python + +Our authentication policy is expecting a new setting, ``auth.secret``. Open +the file ``development.ini`` and add the highlighted line below: + +.. literalinclude:: src/authentication/development.ini + :lines: 18-20 + :emphasize-lines: 3 + :lineno-match: + :language: ini + +Finally, best practices tell us to use a different secret for production, so +open ``production.ini`` and add a different secret: + +.. literalinclude:: src/authentication/production.ini + :lines: 15-17 + :emphasize-lines: 3 + :lineno-match: + :language: ini + + +Add permission checks +~~~~~~~~~~~~~~~~~~~~~ + +:app:`Pyramid` has full support for declarative authorization, which we'll +cover in the next chapter. However, many people looking to get their feet wet +are just interested in authentication with some basic form of home-grown +authorization. We'll show below how to accomplish the simple security goals of +our wiki, now that we can track the logged-in state of users. + +Remember our goals: + +* Allow only ``editor`` and ``basic`` logged-in users to create new pages. +* Only allow ``editor`` users and the page creator (possibly a ``basic`` user) + to edit pages. + +Open the file ``tutorial/views/default.py`` and fix the following imports: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 5-13 + :lineno-match: + :emphasize-lines: 2,9 + :language: python + +Change the two highlighted lines. + +In the same file, now edit the ``edit_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 45-60 + :lineno-match: + :emphasize-lines: 5-7 + :language: python + +Only the highlighted lines need to be changed. + +If the user either is not logged in or the user is not the page's creator +*and* not an ``editor``, then we raise ``HTTPForbidden``. + +In the same file, now edit the ``add_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 62-76 + :lineno-match: + :emphasize-lines: 3-5,13 + :language: python + +Only the highlighted lines need to be changed. + +If the user either is not logged in or is not in the ``basic`` or ``editor`` +roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" +response to the user. However, we will hook this later to redirect to the login +page. Also, now that we have ``request.user``, we no longer have to hard-code +the creator as the ``editor`` user, so we can finally drop that hack. + +These simple checks should protect our views. + + +Login, logout +------------- + +Now that we've got the ability to detect logged-in users, we need to add the +``/login`` and ``/logout`` views so that they can actually login and logout! + + +Add routes for ``/login`` and ``/logout`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Go back to ``tutorial/routes.py`` and add these two routes as highlighted: + +.. literalinclude:: src/authentication/tutorial/routes.py + :lines: 3-6 + :lineno-match: + :emphasize-lines: 2-3 + :language: python + +.. note:: The preceding lines must be added *before* the following + ``view_page`` route definition: + + .. literalinclude:: src/authentication/tutorial/routes.py + :lines: 6 + :lineno-match: + :language: python + + This is because ``view_page``'s route definition uses a catch-all + "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`), + which will catch any route that was not already caught by any route + registered before it. Hence, for ``login`` and ``logout`` views to + have the opportunity of being matched (or "caught"), they must be above + ``/{pagename}``. + + +Add login, logout, and forbidden views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/views/auth.py``, and add the following code to it: + +.. literalinclude:: src/authentication/tutorial/views/auth.py + :linenos: + :language: python + +This code adds three new views to the application: + +- The ``login`` view renders a login form and processes the post from the + login form, checking credentials against our ``users`` table in the database. + + The check is done by first finding a ``User`` record in the database, then + using our ``user.check_password`` method to compare the hashed passwords. + + If the credentials are valid, then we use our authentication policy to store + the user's id in the response using :meth:`pyramid.security.remember`. + + Finally, the user is redirected back to either the page which they were + trying to access (``next``) or the front page as a fallback. This parameter + is used by our forbidden view, as explained below, to finish the login + workflow. + +- The ``logout`` view handles requests to ``/logout`` by clearing the + credentials using :meth:`pyramid.security.forget`, then redirecting them to + the front page. + +- The ``forbidden_view`` is registered using the + :class:`pyramid.view.forbidden_view_config` decorator. This is a special + :term:`exception view`, which is invoked when a + :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised. + + This view will handle a forbidden error by redirecting the user to + ``/login``. As a convenience, it also sets the ``next=`` query string to the + current URL (the one that is forbidding access). This way, if the user + successfully logs in, they will be sent back to the page which they had been + trying to access. + + +Add the ``login.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/login.jinja2`` with the following content: + +.. literalinclude:: src/authentication/tutorial/templates/login.jinja2 + :language: html + +The above template is referenced in the login view that we just added in +``tutorial/views/auth.py``. + + +Add "Login" and "Logout" links +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/templates/layout.jinja2`` and add the following code as +indicated by the highlighted lines. + +.. literalinclude:: src/authentication/tutorial/templates/layout.jinja2 + :lines: 35-46 + :lineno-match: + :emphasize-lines: 2-10 + :language: html + +The ``request.user`` will be ``None`` if the user is not authenticated, or a +``tutorial.models.User`` object if the user is authenticated. This check will +make the logout link shown only when the user is logged in, and conversely the +login link is only shown when the user is logged out. + + +Viewing the application in a browser +------------------------------------ + +We can finally examine our application in a browser (See +:ref:`wiki2-start-the-application`). Launch a browser and visit each of the +following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` page object. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. + +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. + +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. + +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. + +- After logging in (as a result of hitting an edit or add page and submitting + the login form with the ``editor`` credentials), we'll see a "Logout" link in + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 76ce4b83f..234f40e3b 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -1,311 +1,263 @@ .. _wiki2_adding_authorization: ==================== -Adding Authorization +Adding authorization ==================== -Our application currently allows anyone with access to the server to -view, edit, and add pages to our wiki. For purposes of demonstration -we'll change our application to allow only people whom possess a -specific username (`editor`) to add and edit wiki pages but we'll -continue allowing anyone with access to the server to view pages. -:app:`Pyramid` provides facilities for :term:`authorization` and -:term:`authentication`. We'll make use of both features to provide security -to our application. - -We will add an :term:`authentication policy` and an -:term:`authorization policy` to our :term:`application -registry`, add a ``security.py`` module, create a :term:`root factory` -with an :term:`ACL`, and add :term:`permission` declarations to -the ``edit_page`` and ``add_page`` views. - -Then we will add ``login`` and ``logout`` views, and modify the -existing views to make them return a ``logged_in`` flag to the -renderer. - -Finally, we will add a ``login.pt`` template and change the existing -``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. - -The source code for this tutorial stage can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/authorization/>`_. - -Changing ``__init__.py`` For Authorization -------------------------------------------- - -We're going to be making several changes to our ``__init__.py`` file which -will help us configure an authorization policy. - -Adding A Root Factory -~~~~~~~~~~~~~~~~~~~~~ - -We're going to start to use a custom :term:`root factory` within our -``__init__.py`` file. The objects generated by the root factory will be used -as the :term:`context` of each request to our application. We do this to -allow :app:`Pyramid` declarative security to work properly. The context -object generated by the root factory during a request will be decorated with -security declarations. When we begin to use a custom root factory to generate -our contexts, we can begin to make use of the declarative security features -of :app:`Pyramid`. - -We'll modify our ``__init__.py``, passing in a :term:`root factory` to our -:term:`Configurator` constructor. We'll point it at a new class we create -inside our ``models.py`` file. Add the following statements to your -``models.py`` file: - -.. literalinclude:: src/authorization/tutorial/models.py - :lines: 3-4,45-49 - :linenos: - :language: python +In the last chapter we built :term:`authentication` into our wiki. We also +went one step further and used the ``request.user`` object to perform some +explicit :term:`authorization` checks. This is fine for a lot of applications, +but :app:`Pyramid` provides some facilities for cleaning this up and decoupling +the constraints from the view function itself. -The ``RootFactory`` class we've just added will be used by :app:`Pyramid` to -construct a ``context`` object. The context is attached to the request -object passed to our view callables as the ``context`` attribute. - -The context object generated by our root factory will possess an ``__acl__`` -attribute that allows :data:`pyramid.security.Everyone` (a special principal) -to view all pages, while allowing only a :term:`principal` named -``group:editors`` to edit and add pages. The ``__acl__`` attribute attached -to a context is interpreted specially by :app:`Pyramid` as an access control -list during view callable execution. See :ref:`assigning_acls` for more -information about what an :term:`ACL` represents. - -.. note: Although we don't use the functionality here, the ``factory`` used - to create route contexts may differ per-route as opposed to globally. See - the ``factory`` argument to - :meth:`pyramid.config.Configurator.add_route` for more info. - -We'll pass the ``RootFactory`` we created in the step above in as the -``root_factory`` argument to a :term:`Configurator`. - -Configuring an Authorization Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For any :app:`Pyramid` application to perform authorization, we need to add a -``security.py`` module (we'll do that shortly) and we'll need to change our -``__init__.py`` file to add an :term:`authentication policy` and an -:term:`authorization policy` which uses the ``security.py`` file for a -*callback*. - -We'll change our ``__init__.py`` file to enable an -``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable -declarative security checking. We need to import the new policies: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 2-3,8 - :linenos: - :language: python +We will implement access control with the following steps: -Then, we'll add those policies to the configuration: +* Update the :term:`authentication policy` to break down the :term:`userid` + into a list of :term:`principals <principal>` (``security.py``). +* Define an :term:`authorization policy` for mapping users, resources and + permissions (``security.py``). +* Add new :term:`resource` definitions that will be used as the :term:`context` + for the wiki pages (``routes.py``). +* Add an :term:`ACL` to each resource (``routes.py``). +* Replace the inline checks on the views with :term:`permission` declarations + (``views/default.py``). -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 15-21 - :linenos: - :language: python -Note that that the -:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor -accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string -representing an encryption key used by the "authentication ticket" machinery -represented by this policy: it is required. The ``callback`` is a -``groupfinder`` function in the current directory's ``security.py`` file. We -haven't added that module yet, but we're about to. +Add user principals +------------------- -We'll also change ``__init__.py``, adding a call to -:meth:`pyramid.config.Configurator.add_view` that points at our ``login`` -:term:`view callable`. This is also known as a :term:`forbidden view`: +A :term:`principal` is a level of abstraction on top of the raw :term:`userid` +that describes the user in terms of its capabilities, roles, or other +identifiers that are easier to generalize. The permissions are then written +against the principals without focusing on the exact user involved. -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 25,41-43 - :linenos: - :language: python - -A forbidden view configures our newly created login view to show up when -:app:`Pyramid` detects that a view invocation can not be authorized. +:app:`Pyramid` defines two builtin principals used in every application: +:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`. +On top of these we have already mentioned the required principals for this +application in the original design. The user has two possible roles: ``editor`` +or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing +with any other types of principals. -A ``logout`` :term:`view callable` will allow users to log out later: +Open the file ``tutorial/security.py`` and edit it as follows: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 26,34 +.. literalinclude:: src/authorization/tutorial/security.py :linenos: + :emphasize-lines: 3-6,17-24 :language: python -We'll also add ``permission`` arguments with the value ``edit`` to the -``edit_page`` and ``add_page`` views. This indicates that the view -callables which these views reference cannot be invoked without the -authenticated user possessing the ``edit`` permission with respect to the -current context. +Only the highlighted lines need to be added. -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 37-40 - :linenos: - :language: python +Note that the role comes from the ``User`` object. We also add the ``user.id`` +as a principal for when we want to allow that exact user to edit pages which +they have created. -Adding these ``permission`` arguments causes Pyramid to make the -assertion that only users who possess the effective ``edit`` permission at -the time of the request may invoke those two views. We've granted the -``group:editors`` principal the ``edit`` permission at the root model via its -ACL, so only the a user whom is a member of the group named ``group:editors`` -will able to invoke the views associated with the ``add_page`` or -``edit_page`` routes. -Viewing Your Changes -~~~~~~~~~~~~~~~~~~~~ +Add the authorization policy +---------------------------- -When we're done configuring a root factory, adding an authorization policy, -and adding views, your application's ``__init__.py`` will look like this: +We already added the :term:`authorization policy` in the previous chapter +because :app:`Pyramid` requires one when adding an +:term:`authentication policy`. However, it was not used anywhere, so we'll +mention it now. -.. literalinclude:: src/authorization/tutorial/__init__.py - :linenos: +In the file ``tutorial/security.py``, notice the following lines: + +.. literalinclude:: src/authorization/tutorial/security.py + :lines: 38-40 + :lineno-match: + :emphasize-lines: 2 :language: python -Adding ``security.py`` +We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy`, which +will suffice for most applications. It uses the :term:`context` to define the +mapping between a :term:`principal` and :term:`permission` for the current +request via the ``__acl__``. + + +Add resources and ACLs ---------------------- -Add a ``security.py`` module within your package (in the same directory as -:file:`__init__.py`, :file:`views.py`, etc.) with the following content: +Resources are the hidden gem of :app:`Pyramid`. You've made it! -.. literalinclude:: src/authorization/tutorial/security.py +Every URL in a web application represents a :term:`resource` (the "R" in +Uniform Resource Locator). Often the resource is something in your data model, +but it could also be an abstraction over many models. + +Our wiki has two resources: + +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any + logged-in user, having either role of ``basic`` or ``editor``, can create + pages. + +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + ``editor`` users, as well as the original creator of the ``Page``, may edit + the ``PageResource``. Anyone may view it. + +.. note:: + + The wiki data model is simple enough that the ``PageResource`` is mostly + redundant with our ``models.Page`` SQLAlchemy class. It is completely valid + to combine these into one class. However, for this tutorial, they are + explicitly separated to make clear the distinction between the parts about + which :app:`Pyramid` cares versus application-defined objects. + +There are many ways to define these resources, and they can even be grouped +into collections with a hierarchy. However, we're keeping it simple here! + +Open the file ``tutorial/routes.py`` and edit the following lines: + +.. literalinclude:: src/authorization/tutorial/routes.py :linenos: + :emphasize-lines: 1-11,17- :language: python -The ``groupfinder`` function defined here is an :term:`authentication policy` -"callback"; it is a callable that accepts a userid and a request. If -the userid exists in the system, the callback will return a sequence -of group identifiers (or an empty sequence if the user isn't a member -of any groups). If the userid *does not* exist in the system, the -callback will return ``None``. In a production system, user and group -data will most often come from a database, but here we use "dummy" -data to represent user and groups sources. Note that the ``editor`` -user is a member of the ``group:editors`` group in our dummy group -data (the ``GROUPS`` data structure). - -We've given the ``editor`` user membership to the ``group:editors`` by -mapping him to this group in the ``GROUPS`` data structure (``GROUPS = -{'editor':['group:editors']}``). Since the ``groupfinder`` function -consults the ``GROUPS`` data structure, this will mean that, as a -result of the ACL attached to the root returned by the root factory, -and the permission associated with the ``add_page`` and ``edit_page`` -views, the ``editor`` user should be able to add and edit pages. - -Adding Login and Logout Views ------------------------------ - -We'll add a ``login`` view callable which renders a login form and -processes the post from the login form, checking credentials. - -We'll also add a ``logout`` view callable to our application and -provide a link to it. This view will clear the credentials of the -logged in user and redirect back to the front page. - -We'll add a different file (for presentation convenience) to add login -and the logout view callables. Add a file named ``login.py`` to your -application (in the same directory as ``views.py``) with the following -content: - -.. literalinclude:: src/authorization/tutorial/login.py - :linenos: +The highlighted lines need to be edited or added. + +The ``NewPage`` class has an ``__acl__`` on it that returns a list of mappings +from :term:`principal` to :term:`permission`. This defines *who* can do *what* +with that :term:`resource`. In our case we want to allow only those users with +the principals of either ``role:editor`` or ``role:basic`` to have the +``create`` permission: + +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 30-38 + :lineno-match: + :emphasize-lines: 5-9 :language: python -Changing Existing Views ------------------------ +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by +declaring a ``factory`` on the route: -Then we need to change each of our ``view_page``, ``edit_page`` and -``add_page`` views in ``views.py`` to pass a "logged in" parameter to its -template. We'll add something like this to each view body: +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 18-19 + :lineno-match: + :emphasize-lines: 1-2 + :language: python -.. ignore-next-block -.. code-block:: python - :linenos: +The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine *who* can do *what* to the page. - from pyramid.security import authenticated_userid - logged_in = authenticated_userid(request) +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 47- + :lineno-match: + :emphasize-lines: 5-10 + :language: python -We'll then change the return value of these views to pass the `resulting -`logged_in`` value to the template, e.g.: +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and +``edit_page`` routes by declaring a ``factory`` on the routes: -.. ignore-next-block -.. code-block:: python - :linenos: +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 17-21 + :lineno-match: + :emphasize-lines: 1,4-5 + :language: python - return dict(page = context, - content = content, - logged_in = logged_in, - edit_url = edit_url) -Adding the ``login.pt`` Template --------------------------------- +Add view permissions +-------------------- -Add a ``login.pt`` template to your templates directory. It's -referred to within the login view we just added to ``login.py``. +At this point we've modified our application to load the ``PageResource``, +including the actual ``Page`` model in the ``page_factory``. The +``PageResource`` is now the :term:`context` for all ``view_page`` and +``edit_page`` views. Similarly the ``NewPage`` will be the context for the +``add_page`` view. -.. literalinclude:: src/authorization/tutorial/templates/login.pt - :language: xml +Open the file ``tutorial/views/default.py``. -Change ``view.pt`` and ``edit.pt`` ----------------------------------- +First, you can drop a few imports that are no longer necessary: + +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 5-7 + :lineno-match: + :emphasize-lines: 1 + :language: python -We'll also need to change our ``edit.pt`` and ``view.pt`` templates to -display a "Logout" link if someone is logged in. This link will -invoke the logout view. +Edit the ``view_page`` view to declare the ``view`` permission, and remove the +explicit checks within the view: -To do so we'll add this to both templates within the ``<div id="right" -class="app-welcome align-right">`` div: +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 18-23 + :lineno-match: + :emphasize-lines: 1-2,4 + :language: python -.. code-block:: xml +The work of loading the page has already been done in the factory, so we can +just pull the ``page`` object out of the ``PageResource``, loaded as +``request.context``. Our factory also guarantees we will have a ``Page``, as it +raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying +the view logic. - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> +Edit the ``edit_page`` view to declare the ``edit`` permission: -Seeing Our Changes To ``views.py`` and our Templates ----------------------------------------------------- +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 38-42 + :lineno-match: + :emphasize-lines: 1-2,4 + :language: python -Our ``views.py`` module will look something like this when we're done: +Edit the ``add_page`` view to declare the ``create`` permission: -.. literalinclude:: src/authorization/tutorial/views.py - :linenos: +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 52-56 + :lineno-match: + :emphasize-lines: 1-2,4 :language: python -Our ``edit.pt`` template will look something like this when we're done: +Note the ``pagename`` here is pulled off of the context instead of +``request.matchdict``. The factory has done a lot of work for us to hide the +actual route pattern. -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :language: xml +The ACLs defined on each :term:`resource` are used by the :term:`authorization +policy` to determine if any :term:`principal` is allowed to have some +:term:`permission`. If this check fails (for example, the user is not logged +in) then an ``HTTPForbidden`` exception will be raised automatically. Thus +we're able to drop those exceptions and checks from the views themselves. +Rather we've defined them in terms of operations on a resource. -Our ``view.pt`` template will look something like this when we're done: +The final ``tutorial/views/default.py`` should look like the following: -.. literalinclude:: src/authorization/tutorial/templates/view.pt - :language: xml +.. literalinclude:: src/authorization/tutorial/views/default.py + :linenos: + :language: python -Viewing the Application in a Browser +Viewing the application in a browser ------------------------------------ -We can finally examine our application in a browser. The views we'll -try are as follows: - -- Visiting ``http://localhost:6543/`` in a browser invokes the - ``view_wiki`` view. This always redirects to the ``view_page`` view - of the FrontPage page object. It is executable by any user. - -- Visiting ``http://localhost:6543/FrontPage`` in a browser invokes - the ``view_page`` view of the FrontPage page object. - -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser - invokes the edit view for the FrontPage object. It is executable by - only the ``editor`` user. If a different user (or the anonymous - user) invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will - display the edit page form. - -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a page. It is executable by only - the ``editor`` user. If a different user (or the anonymous user) - invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will - display the edit page form. - -- After logging in (as a result of hitting an edit or add page - and submitting the login form with the ``editor`` - credentials), we'll see a Logout link in the upper right hand - corner. When we click it, we're logged out, and redirected - back to the front page. +We can finally examine our application in a browser (See +:ref:`wiki2-start-the-application`). Launch a browser and visit each of the +following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` page object. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. + +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. + +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. + +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. + +- After logging in (as a result of hitting an edit or add page and submitting + the login form with the ``editor`` credentials), we'll see a "Logout" link in + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst index 880b5b219..ee7dfe36f 100644 --- a/docs/tutorials/wiki2/background.rst +++ b/docs/tutorials/wiki2/background.rst @@ -1,17 +1,22 @@ +.. _wiki2_background: + ========== Background ========== -This tutorial presents a :app:`Pyramid` application that uses -technologies which will be familiar to someone with :term:`Pylons` -experience. It uses :term:`SQLAlchemy` as a persistence mechanism and -:term:`url dispatch` to map URLs to code. It can also be followed by -people without any prior Python web framework experience. +This version of the :app:`Pyramid` wiki tutorial presents a +:app:`Pyramid` application that uses technologies which will be +familiar to someone with SQL database experience. It uses +:term:`SQLAlchemy` as a persistence mechanism and :term:`URL dispatch` to map +URLs to code. It can also be followed by people without any prior +Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* he will need a Windows system of any kind. +variant, etc.) *or* a Windows system of any kind. + +.. note:: -This tutorial is targeted at :app:`Pyramid` version 1.0. + This tutorial runs on both Python 2 and 3 without modification. Have fun! 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. diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 7aa2214fc..6520613ea 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -1,95 +1,263 @@ +.. _wiki2_defining_the_domain_model: + ========================= Defining the Domain Model ========================= -The first change we'll make to our stock paster-generated application will be -to define a :term:`domain model` constructor representing a wiki page. We'll -do this inside our ``models.py`` file. +The first change we'll make to our stock ``pcreate``-generated application will +be to define a wiki page :term:`domain model`. + +.. note:: -The source code for this tutorial stage can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/models/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/models/>`_. + There is nothing special about the filename ``user.py`` or ``page.py`` except + that they are Python modules. A project may have many models throughout its + codebase in arbitrarily named modules. Modules implementing models often + have ``model`` in their names or they may live in a Python subpackage of + your application package named ``models`` (as we've done in this tutorial), + but this is only a convention and not a requirement. -Making Edits to ``models.py`` ------------------------------ -.. note:: +Declaring dependencies in our ``setup.py`` file +=============================================== - There is nothing automagically special about the filename - ``models.py``. A project may have many models throughout its - codebase in arbitrarily-named files. Files implementing models - often have ``model`` in their filenames (or they may live in a - Python subpackage of your application package named ``models``) , - but this is only by convention. +The models code in our application will depend on a package which is not a +dependency of the original "tutorial" application. The original "tutorial" +application was generated by the ``pcreate`` command; it doesn't know about our +custom application requirements. -The first thing we want to do is remove the stock ``MyModel`` class from the -generated ``models.py`` file. The ``MyModel`` class is only a sample and -we're not going to use it. +We need to add a dependency, the ``bcrypt`` package, to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. -Next, we'll remove the :class:`sqlalchemy.Unicode` import and replace it -with :class:`sqlalchemy.Text`. +Open ``tutorial/setup.py`` and edit it to look like the following: -.. literalinclude:: src/models/tutorial/models.py - :lines: 5 +.. literalinclude:: src/models/setup.py + :linenos: + :emphasize-lines: 12 + :language: python + +Only the highlighted line needs to be added. + + +Running ``pip install -e .`` +============================ + +Since a new software dependency was added, you will need to run ``pip install +-e .`` again inside the root of the ``tutorial`` package to obtain and register +the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which ``setup.py`` lives) and execute the following command. + +On UNIX: + +.. code-block:: bash + + $ cd tutorial + $ $VENV/bin/pip install -e . + +On Windows: + +.. code-block:: doscon + + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . + +Success executing this command will end with a line to the console something +like this:: + + Successfully installed bcrypt-2.0.0 cffi-1.5.2 pycparser-2.14 tutorial-0.0 + + +Remove ``mymodel.py`` +--------------------- + +Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is +only a sample and we're not going to use it. + + +Add ``user.py`` +--------------- + +Create a new file ``tutorial/models/user.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/user.py :linenos: :language: py -Then, we'll add a ``Page`` class. Because this is a SQLAlchemy -application, this class should inherit from an instance of -:class:`sqlalchemy.ext.declarative.declarative_base`. Declarative -SQLAlchemy models are easier to use than directly-mapped ones. +This is a very basic model for a user who can authenticate with our wiki. + +We discussed briefly in the previous chapter that our models will inherit from +an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will +attach the model to our schema. + +As you can see, our ``User`` class has a class-level attribute +``__tablename__`` which equals the string ``users``. Our ``User`` class will +also have class-level attributes named ``id``, ``name``, ``password_hash``, +and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will +map to columns in the ``users`` table. The ``id`` attribute will be the primary +key in the table. The ``name`` attribute will be a text column, each value of +which needs to be unique within the column. The ``password_hash`` is a nullable +text attribute that will contain a securely hashed password [1]_. Finally, the +``role`` text attribute will hold the role of the user. + +There are two helper methods that will help us later when using the user +objects. The first is ``set_password`` which will take a raw password and +transform it using bcrypt_ into an irreversible representation, a process known +as "hashing". The second method, ``check_password``, will allow us to compare +the hashed value of the submitted password against the hashed value of the +password stored in the user's record in the database. If the two hashed values +match, then the submitted password is valid, and we can authenticate the user. + +We hash passwords so that it is impossible to decrypt them and use them to +authenticate in the application. If we stored passwords foolishly in clear +text, then anyone with access to the database could retrieve any password to +authenticate as any user. + -.. literalinclude:: src/models/tutorial/models.py - :pyobject: Page +Add ``page.py`` +--------------- + +Create a new file ``tutorial/models/page.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/page.py :linenos: - :language: python + :language: py + +As you can see, our ``Page`` class is very similar to the ``User`` defined +above, except with attributes focused on storing information about a wiki page, +including ``id``, ``name``, and ``data``. The only new construct introduced +here is the ``creator_id`` column, which is a foreign key referencing the +``users`` table. Foreign keys are very useful at the schema-level, but since we +want to relate ``User`` objects with ``Page`` objects, we also define a +``creator`` attribute as an ORM-level mapping between the two tables. +SQLAlchemy will automatically populate this value using the foreign key +referencing the user. Since the foreign key has ``nullable=False``, we are +guaranteed that an instance of ``page`` will have a corresponding +``page.creator``, which will be a ``User`` instance. + + +Edit ``models/__init__.py`` +--------------------------- + +Since we are using a package for our models, we also need to update our +``__init__.py`` file to ensure that the models are attached to the metadata. -As you can see, our ``Page`` class has a class level attribute -``__tablename__`` which equals the string ``'pages'``. This means that -SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our Page -class will also have class-level attributes named ``id``, ``name`` and -``data`` (all instances of :class:`sqlalchemy.Column`). These will map to -columns in the ``pages`` table. The ``id`` attribute will be the primary key -in the table. The ``name`` attribute will be a text attribute, each value of -which needs to be unique within the column. The ``data`` attribute is a text -attribute that will hold the body of each page. - -We'll also remove our ``populate`` function. We'll inline the populate step -into ``initialize_sql``, changing our ``initialize_sql`` function to add a -FrontPage object to our database at startup time. - -.. literalinclude:: src/models/tutorial/models.py - :pyobject: initialize_sql +Open the ``tutorial/models/__init__.py`` file and edit it to look like +the following: + +.. literalinclude:: src/models/tutorial/models/__init__.py :linenos: - :language: python + :language: py + :emphasize-lines: 8,9 -Here, we're using a slightly different binding syntax. It is otherwise -largely the same as the ``initialize_sql`` in the paster-generated -``models.py``. +Here we align our imports with the names of the models, ``Page`` and ``User``. -Our ``DBSession`` assignment stays the same as the original generated -``models.py``. -Looking at the Result of all Our Edits to ``models.py`` -------------------------------------------------------- +Edit ``scripts/initializedb.py`` +-------------------------------- -The result of all of our edits to ``models.py`` will end up looking -something like this: +We haven't looked at the details of this file yet, but within the ``scripts`` +directory of your ``tutorial`` package is a file named ``initializedb.py``. +Code in this file is executed whenever we run the ``initialize_tutorial_db`` +command, as we did in the installation step of this tutorial [2]_. -.. literalinclude:: src/models/tutorial/models.py +Since we've changed our model, we need to make changes to our +``initializedb.py`` script. In particular, we'll replace our import of +``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end +of the script to create two ``User`` objects (``basic`` and ``editor``) as well +as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``. + +Open ``tutorial/scripts/initializedb.py`` and edit it to look like the +following: + +.. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python + :emphasize-lines: 18,44-57 + +Only the highlighted lines need to be changed. -Viewing the Application in a Browser ------------------------------------- + +Installing the project and re-initializing the database +------------------------------------------------------- + +Because our model has changed, and in order to reinitialize the database, we +need to rerun the ``initialize_tutorial_db`` command to pick up the changes +we've made to both the models.py file and to the initializedb.py file. See +:ref:`initialize_db_wiki2` for instructions. + +Success will look something like this: + +.. code-block:: bash + + 2016-04-09 02:49:51,711 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 02:49:51,711 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 02:49:51,712 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 02:49:51,712 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 02:49:51,713 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages") + 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users") + 2016-04-09 02:49:51,714 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 02:49:51,715 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE users ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + role TEXT NOT NULL, + password_hash TEXT, + CONSTRAINT pk_users PRIMARY KEY (id), + CONSTRAINT uq_users_name UNIQUE (name) + ) + + + 2016-04-09 02:49:51,715 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE pages ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + data INTEGER NOT NULL, + creator_id INTEGER NOT NULL, + CONSTRAINT pk_pages PRIMARY KEY (id), + CONSTRAINT uq_pages_name UNIQUE (name), + CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id) + ) + + + 2016-04-09 02:49:51,716 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 02:49:51,717 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 02:49:52,256 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-04-09 02:49:52,257 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-04-09 02:49:52,257 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', b'$2b$12$APUPJvI/kKxrbQPyQehkR.ggoOM6fFYCZ07SFCkWGltl1wJsKB98y') + 2016-04-09 02:49:52,258 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-04-09 02:49:52,258 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', b'$2b$12$GeFnypuQpZyxZLH.sN0akOrPdZMcQjqVTCim67u6f89lOFH/0ddc6') + 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?) + 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1) + 2016-04-09 02:49:52,259 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + + +View the application in a browser +--------------------------------- We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the -application successfully. If you try to start the application, you'll wind -up with a Python traceback on your console that ends with this exception: +application successfully. If you try to start the application (see +:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on +your console that ends with this exception: .. code-block:: text ImportError: cannot import name MyModel This will also happen if you attempt to run the tests. + +.. _bcrypt: https://pypi.python.org/pypi/bcrypt + +.. [1] We are using the bcrypt_ package from PyPI to hash our passwords + securely. There are other one-way hash algorithms for passwords if + bcrypt is an issue on your system. Just make sure that it's an + algorithm approved for storing passwords versus a generic one-way hash. + +.. [2] The command is named ``initialize_tutorial_db`` because of the mapping + defined in the ``[console_scripts]`` entry point of our project's + ``setup.py`` file. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index cea376b77..996bff88c 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -1,343 +1,479 @@ +.. _wiki2_defining_views: + ============== Defining Views ============== -A :term:`view callable` in a :term:`url dispatch` -based :app:`Pyramid` -application is typically a simple Python function that accepts a single -parameter named :term:`request`. A view callable is assumed to return a -:term:`response` object. - -.. note:: A :app:`Pyramid` view can also be defined as callable - which accepts *two* arguments: a :term:`context` and a - :term:`request`. You'll see this two-argument pattern used in - other :app:`Pyramid` tutorials and applications. Either calling - convention will work in any :app:`Pyramid` application; the - calling conventions can be used interchangeably as necessary. In - :term:`url dispatch` based applications, however, the context - object is rarely used in the view body itself, so within this - tutorial we define views as callables that accept only a request to - avoid the visual "noise". If you do need the ``context`` within a - view function that only takes the request as a single argument, you - can obtain it via ``request.context``. - -The request passed to every view that is called as the result of a route -match has an attribute named ``matchdict`` that contains the elements placed -into the URL by the ``pattern`` of a ``route`` statement. For instance, if a -call to :meth:`pyramid.config.Configurator.add_route` in ``__init__.py`` had -the pattern ``{one}/{two}``, and the URL at ``http://example.com/foo/bar`` -was invoked, matching this pattern, the ``matchdict`` dictionary attached to -the request passed to the view would have a ``'one'`` key with the value -``'foo'`` and a ``'two'`` key with the value ``'bar'``. - -The source code for this tutorial stage can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/>`_. - -Declaring Dependencies in Our ``setup.py`` File -=============================================== - -The view code in our application will depend on a package which is not a -dependency of the original "tutorial" application. The original "tutorial" -application was generated by the ``paster create`` command; it doesn't know -about our custom application requirements. We need to add a dependency on -the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by -assigning this dependency to the ``install_requires`` parameter in the -``setup`` function. - -Our resulting ``setup.py`` should look like so: +A :term:`view callable` in a :app:`Pyramid` application is typically a simple +Python function that accepts a single parameter named :term:`request`. A view +callable is assumed to return a :term:`response` object. + +The request object has a dictionary as an attribute named ``matchdict``. A +``matchdict`` maps the placeholders in the matching URL ``pattern`` to the +substrings of the path in the :term:`request` URL. For instance, if a call to +:meth:`pyramid.config.Configurator.add_route` has the pattern ``/{one}/{two}``, +and a user visits ``http://example.com/foo/bar``, our pattern would be matched +against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo', +'two':'bar'}``. + + +Adding the ``docutils`` dependency +================================== + +Remember in the previous chapter we added a new dependency of the ``bcrypt`` +package. Again, the view code in our application will depend on a package which +is not a dependency of the original "tutorial" application. + +We need to add a dependency on the ``docutils`` package to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. + +Open ``tutorial/setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py :linenos: + :emphasize-lines: 13 :language: python -.. note:: After these new dependencies are added, you will need to - rerun ``python setup.py develop`` inside the root of the - ``tutorial`` package to obtain and register the newly added - dependency package. +Only the highlighted line needs to be added. + +Again, as we did in the previous chapter, the dependency now needs to be +installed, so re-run the ``$VENV/bin/pip install -e .`` command. + + +Static assets +------------- + +Our templates name static assets, including CSS and images. We don't need +to create these files within our package's ``static`` directory because they +were provided at the time we created the project. + +As an example, the CSS file will be accessed via +``http://localhost:6543/static/theme.css`` by virtue of the call to the +``add_static_view`` directive we've made in the ``routes.py`` file. Any number +and type of static assets can be placed in this directory (or subdirectories) +and are just referred to by URL or by using the convenience method +``static_url``, e.g., ``request.static_url('<package>:static/foo.css')`` within +templates. + + +Adding routes to ``routes.py`` +============================== + +This is the `URL Dispatch` tutorial, so let's start by adding some URL patterns +to our app. Later we'll attach views to handle the URLs. + +The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route` +calls which serve to add routes to our application. First we'll get rid of the +existing route created by the template using the name ``'home'``. It's only an +example and isn't relevant to our application. -Adding View Functions -===================== +We then need to add four calls to ``add_route``. Note that the *ordering* of +these declarations is very important. Route declarations are matched in the +order they're registered. -We'll get rid of our ``my_view`` view function in our ``views.py`` file. -It's only an example and isn't relevant to our application. +#. Add a declaration which maps the pattern ``/`` (signifying the root URL) to + the route named ``view_wiki``. In the next step, we will map it to our + ``view_wiki`` view callable by virtue of the ``@view_config`` decorator + attached to the ``view_wiki`` view function, which in turn will be indicated + by ``route_name='view_wiki'``. -Then we're going to add four :term:`view callable` functions to our -``views.py`` module. One view callable (named ``view_wiki``) will display -the wiki itself (it will answer on the root URL), another named ``view_page`` -will display an individual page, another named ``add_page`` will allow a page -to be added, and a final view callable named ``edit_page`` will allow a page -to be edited. We'll describe each one briefly and show the resulting -``views.py`` file afterward. +#. Add a declaration which maps the pattern ``/{pagename}`` to the route named + ``view_page``. This is the regular view for a page. Again, in the next step, + we will map it to our ``view_page`` view callable by virtue of the + ``@view_config`` decorator attached to the ``view_page`` view function, + whin in turn will be indicated by ``route_name='view_page'``. + +#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the + route named ``add_page``. This is the add view for a new page. We will map + it to our ``add_page`` view callable by virtue of the ``@view_config`` + decorator attached to the ``add_page`` view function, which in turn will be + indicated by ``route_name='add_page'``. + +#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the + route named ``edit_page``. This is the edit view for a page. We will map it + to our ``edit_page`` view callable by virtue of the ``@view_config`` + decorator attached to the ``edit_page`` view function, which in turn will be + indicated by ``route_name='edit_page'``. + +As a result of our edits, the ``routes.py`` file should look like the +following: + +.. literalinclude:: src/views/tutorial/routes.py + :linenos: + :emphasize-lines: 3-6 + :language: python + +The highlighted lines are the ones that need to be added or edited. + +.. warning:: + + The order of the routes is important! If you placed + ``/{pagename}/edit_page`` *before* ``/add_page/{pagename}``, then we would + never be able to add pages. This is because the first route would always + match a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` + to have priority. This isn't a huge problem in this particular app because + wiki pages are always camel case, but it's important to be aware of this + behavior in your own apps. + + +Adding view functions in ``views/default.py`` +============================================= + +It's time for a major change. Open ``tutorial/views/default.py`` and +edit it to look like the following: + +.. literalinclude:: src/views/tutorial/views/default.py + :linenos: + :language: python + :emphasize-lines: 1-9,12- + +The highlighted lines need to be added or edited. + +We added some imports, and created a regular expression to find "WikiWords". + +We got rid of the ``my_view`` view function and its decorator that was added +when we originally rendered the ``alchemy`` scaffold. It was only an example +and isn't relevant to our application. We also deleted the ``db_err_msg`` +string. + +Then we added four :term:`view callable` functions to our ``views/default.py`` +module, as mentioned in the previous step: + +* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. +* ``view_page()`` - Displays an individual page. +* ``edit_page()`` - Allows the user to edit a page. +* ``add_page()`` - Allows the user to add a page. + +We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``views.py``. A project may - have many view callables throughout its codebase in arbitrarily-named - files. Files implementing view callables often have ``view`` in their - filenames (or may live in a Python subpackage of your application package - named ``views``), but this is only by convention. + There is nothing special about the filename ``default.py`` exept that it is a + Python module. A project may have many view callables throughout its codebase + in arbitrarily named modules. Modules implementing view callables often have + ``view`` in their name (or may live in a Python subpackage of your + application package named ``views``, as in our case), but this is only by + convention, not a requirement. + The ``view_wiki`` view function ------------------------------- -The ``view_wiki`` function is the :term:`default view` that will be called -when a request is made to the root URL of our wiki. It always redirects to -a URL which represents the path to our "FrontPage". +Following is the code for the ``view_wiki`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :pyobject: view_wiki +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 17-20 + :lineno-match: :linenos: :language: python -The ``view_wiki`` function returns an instance of the +``view_wiki()`` is the :term:`default view` that gets called when a request is +made to the root URL of our wiki. It always redirects to a URL which +represents the path to our "FrontPage". + +The ``view_wiki`` view callable always redirects to the URL of a Page resource +named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the -:func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` -page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the -"location" of the HTTPFound response, forming an HTTP redirect. +the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.response.Response`). It uses the +:meth:`pyramid.request.Request.route_url` API to construct a URL to the +``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as +the "location" of the ``HTTPFound`` response, forming an HTTP redirect. + The ``view_page`` view function ------------------------------- -The ``view_page`` function will be used to show a single page of our -wiki. It renders the :term:`ReStructuredText` body of a page (stored as -the ``data`` attribute of a Page object) as HTML. Then it substitutes an -HTML anchor for each *WikiWord* reference in the rendered HTML using a -compiled regular expression. +Here is the code for the ``view_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :pyobject: view_page +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 22-42 + :lineno-match: :linenos: :language: python -The curried function named ``check`` is used as the first argument to +``view_page()`` is used to display a single page of our wiki. It renders the +:term:`reStructuredText` body of a page (stored as the ``data`` attribute of a +``Page`` model object) as HTML. Then it substitutes an HTML anchor for each +*WikiWord* reference in the rendered HTML using a compiled regular expression. + +The curried function named ``add_link`` is used as the first argument to ``wikiwords.sub``, indicating that it should be called to provide a value for each WikiWord match found in the content. If the wiki already contains a -page with the matched WikiWord name, the ``check`` function generates a view +page with the matched WikiWord name, ``add_link()`` generates a view link to be used as the substitution value and returns it. If the wiki does -not already contain a page with with the matched WikiWord name, the function +not already contain a page with the matched WikiWord name, ``add_link()`` generates an "add" link as the substitution value and returns it. As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of our current page object. -We then generate an edit URL (because it's easier to do here than in the -template), and we return a dictionary with a number of arguments. The fact -that this view returns a dictionary (as opposed to a :term:`response` object) +We then generate an edit URL, because it's easier to do here than in the +template, and we return a dictionary with a number of arguments. The fact that +``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` -associated with the view configuration to render a template. In our case, -the template which will be rendered will be the ``templates/view.pt`` -template, as per the configuration put into effect in ``__init__.py``. - -The ``add_page`` view function ------------------------------- +associated with the view configuration to render a response. In our case, the +renderer used will be the ``view.jinja2`` template, as indicated in +the ``@view_config`` decorator that is applied to ``view_page()``. -The ``add_page`` function will be invoked when a user clicks on a *WikiWord* -which isn't yet represented as a page in the system. The ``check`` function -within the ``view_page`` view generates URLs to this view. It also acts as a -handler for the form that is generated when we want to add a page object. -The ``matchdict`` attribute of the request passed to the ``add_page`` view -will have the values we need to construct URLs and find model objects. +If the page does not exist, then we need to handle that by raising a +:class:`pyramid.httpexceptions.HTTPNotFound` to trigger our 404 handling, +defined in ``tutorial/views/notfound.py``. -.. literalinclude:: src/views/tutorial/views.py - :pyobject: add_page - :linenos: - :language: python +.. note:: -The ``matchdict`` will have a ``'pagename'`` key that matches the name of -the page we'd like to add. If our add view is invoked via, -e.g. ``http://localhost:6543/add_page/SomeName``, the value for -``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. + Using ``raise`` versus ``return`` with the HTTP exceptions is an important + distinction that can commonly mess people up. In + ``tutorial/views/notfound.py`` there is an :term:`exception view` + registered for handling the ``HTTPNotFound`` exception. Exception views are + only triggered for raised exceptions. If the ``HTTPNotFound`` is returned, + then it has an internal "stock" template that it will use to render itself + as a response. If you aren't seeing your exception view being executed, this + is most likely the problem! See :ref:`special_exceptions_in_callables` for + more information about exception views. -If the view execution is *not* a result of a form submission (if the -expression ``'form.submitted' in request.params`` is ``False``), the view -callable renders a template. To do so, it generates a "save url" which the -template uses as the form post URL during rendering. We're lazy here, so -we're trying to use the same template (``templates/edit.pt``) for the add -view as well as the page edit view, so we create a dummy Page object in order -to satisfy the edit form's desire to have *some* page object exposed as -``page``, and :app:`Pyramid` will render the template associated with this -view to a response. - -If the view execution *is* a result of a form submission (if the expression -``'form.submitted' in request.params`` is ``True``), we scrape the page body -from the form data, create a Page object with this page body and the name -taken from ``matchdict['pagename']``, and save it into the database using -``session.add``. We then redirect back to the ``view_page`` view for the -newly created page. The ``edit_page`` view function ------------------------------- -The ``edit_page`` function will be invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form but it also acts as -the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``'pagename'`` key -matching the name of the page the user wants to edit. +Here is the code for the ``edit_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :pyobject: edit_page +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 44-56 + :lineno-match: :linenos: :language: python -If the view execution is *not* a result of a form submission (if the -expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the page object and a ``save_url`` -which will be used as the action of the generated form. +``edit_page()`` is invoked when a user clicks the "Edit this Page" button on +the view form. It renders an edit form, but it also acts as the handler for the +form which it renders. The ``matchdict`` attribute of the request passed to the +``edit_page`` view will have a ``'pagename'`` key matching the name of the page +that the user wants to edit. -If the view execution *is* a result of a form submission (if the expression +If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), the view grabs the ``body`` element of the request parameters and sets it as the ``data`` attribute of the page object. It then redirects to the ``view_page`` view of the wiki page. -Viewing the Result of all Our Edits to ``views.py`` -=================================================== +If the view execution is *not* a result of a form submission (i.e., the +expression ``'form.submitted' in request.params`` is ``False``), the view +simply renders the edit form, passing the page object and a ``save_url`` +which will be used as the action of the generated form. + +.. note:: + + Since our ``request.dbsession`` defined in the previous chapter is + registered with the ``pyramid_tm`` transaction manager, any changes we make + to objects managed by the that session will be committed automatically. In + the event that there was an error (even later, in our template code), the + changes would be aborted. This means the view itself does not need to + concern itself with commit/rollback logic. + + +The ``add_page`` view function +------------------------------ -The result of all of our edits to ``views.py`` will leave it looking -like this: +Here is the code for the ``add_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 58- + :lineno-match: :linenos: :language: python -Adding Templates -================ +``add_page()`` is invoked when a user clicks on a *WikiWord* which isn't yet +represented as a page in the system. The ``add_link`` function within the +``view_page`` view generates URLs to this view. ``add_page()`` also acts as a +handler for the form that is generated when we want to add a page object. The +``matchdict`` attribute of the request passed to the ``add_page()`` view will +have the values we need to construct URLs and find model objects. + +The ``matchdict`` will have a ``'pagename'`` key that matches the name of the +page we'd like to add. If our add view is invoked via, for example, +``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in +the ``matchdict`` will be ``'SomeName'``. + +Next a check is performed to determine whether the ``Page`` already exists in +the database. If it already exists, then the client is redirected to the +``edit_page`` view, else we continue to the next check. + +If the view execution *is* a result of a form submission (i.e., the expression +``'form.submitted' in request.params`` is ``True``), we grab the page body from +the form data, create a Page object with this page body and the name taken from +``matchdict['pagename']``, and save it into the database using +``request.dbession.add``. Since we have not yet covered authentication, we +don't have a logged-in user to add as the page's ``creator``. Until we get to +that point in the tutorial, we'll just assume that all pages are created by the +``editor`` user. Thus we query for that object, and set it on ``page.creator``. +Finally, we redirect the client back to the ``view_page`` view for the newly +created page. + +If the view execution is *not* a result of a form submission (i.e., the +expression ``'form.submitted' in request.params`` is ``False``), the view +callable renders a template. To do so, it generates a ``save_url`` which the +template uses as the form post URL during rendering. We're lazy here, so +we're going to use the same template (``templates/edit.jinja2``) for the add +view as well as the page edit view. To do so we create a dummy ``Page`` object +in order to satisfy the edit form's desire to have *some* page object +exposed as ``page``. :app:`Pyramid` will render the template associated +with this view to a response. -The views we've added all reference a :term:`template`. Each template is a -:term:`Chameleon` :term:`ZPT` template. These templates will live in the -``templates`` directory of our tutorial package. -The ``view.pt`` Template ------------------------- +Adding templates +================ -The ``view.pt`` template is used for viewing a single wiki page. It is used -by the ``view_page`` view function. It should have a div that is "structure -replaced" with the ``content`` value provided by the view. It should also -have a link on the rendered page that points at the "edit" URL (the URL which -invokes the ``edit_page`` view for the page being viewed). +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Jinja2` template. +These templates will live in the ``templates`` directory of our tutorial +package. Jinja2 templates must have a ``.jinja2`` extension to be recognized +as such. -Once we're done with the ``view.pt`` template, it will look a lot like the -below: -.. literalinclude:: src/views/tutorial/templates/view.pt - :language: xml +The ``layout.jinja2`` template +------------------------------ -.. note:: The names available for our use in a template are always - those that are present in the dictionary returned by the view - callable. But our templates make use of a ``request`` object that - none of our tutorial views return in their dictionary. This value - appears as if "by magic". However, ``request`` is one of several - names that are available "by default" in a template when a template - renderer is used. See :ref:`chameleon_template_renderers` for more - information about other names that are available by default in a - template when a Chameleon template is used as a renderer. +Update ``tutorial/templates/layout.jinja2`` with the following content, as +indicated by the emphasized lines: -The ``edit.pt`` Template ------------------------- +.. literalinclude:: src/views/tutorial/templates/layout.jinja2 + :linenos: + :emphasize-lines: 11,35-36 + :language: html -The ``edit.pt`` template is used for adding and editing a wiki page. It is -used by the ``add_page`` and ``edit_page`` view functions. It should display -a page containing a form that POSTs back to the "save_url" argument supplied -by the view. The form should have a "body" textarea field (the page data), -and a submit button that has the name "form.submitted". The textarea in the -form should be filled with any existing page data when it is rendered. +Since we're using a templating engine, we can factor common boilerplate out of +our page templates into reusable components. One method for doing this is +template inheritance via blocks. -Once we're done with the ``edit.pt`` template, it will look a lot like -the below: +- We have defined two placeholders in the layout template where a child + template can override the content. These blocks are named ``subtitle`` (line + 11) and ``content`` (line 36). +- Please refer to the Jinja2_ documentation for more information about template + inheritance. -.. literalinclude:: src/views/tutorial/templates/edit.pt - :language: xml -Static Assets -------------- +The ``view.jinja2`` template +---------------------------- -Our templates name a single static asset named ``pylons.css``. We don't need -to create this file within our package's ``static`` directory because it was -provided at the time we created the project. This file is a little too long -to replicate within the body of this guide, however it is available `online -<http://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css>`_. +Create ``tutorial/templates/view.jinja2`` and add the following content: -This CSS file will be accessed via -e.g. ``http://localhost:6543/static/pylons.css`` by virtue of the call to -``add_static_view`` directive we've made in the ``__init__.py`` file. Any -number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url`` -e.g. ``request.static_url('{{package}}:static/foo.css')`` within templates. +.. literalinclude:: src/views/tutorial/templates/view.jinja2 + :linenos: + :language: html -Mapping Views to URLs in ``__init__.py`` -======================================== +This template is used by ``view_page()`` for displaying a single wiki page. -The ``__init__.py`` file contains -:meth:`pyramid.config.Configurator.add_view` calls which serve to map -routes via :term:`url dispatch` to views. First, we’ll get rid of the -existing route created by the template using the name ``'home'``. It’s only an -example and isn’t relevant to our application. +- We begin by extending the ``layout.jinja2`` template defined above, which + provides the skeleton of the page (line 1). +- We override the ``subtitle`` block from the base layout, inserting the page + name into the page's title (line 3). +- We override the ``content`` block from the base layout to insert our markup + into the body (lines 5-18). +- We use a variable that is replaced with the ``content`` value provided by the + view (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to + prevent escaping it (e.g., changing ">" to ">"). +- We create a link that points at the "edit" URL, which when clicked invokes + the ``edit_page`` view for the requested page (line 9). -We then need to add four calls to ``add_route``. Note that the *ordering* of -these declarations is very important. ``route`` declarations are matched in -the order they're found in the ``__init__.py`` file. -#. Add a declaration which maps the pattern ``/`` (signifying the root URL) - to the route named ``view_wiki``. +The ``edit.jinja2`` template +---------------------------- -#. Add a declaration which maps the pattern ``/{pagename}`` to the route named - ``view_page``. This is the regular view for a page. +Create ``tutorial/templates/edit.jinja2`` and add the following content: -#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. +.. literalinclude:: src/views/tutorial/templates/edit.jinja2 + :linenos: + :emphasize-lines: 1,3,12,14,17 + :language: html -#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. +This template serves two use cases. It is used by ``add_page()`` and +``edit_page()`` for adding and editing a wiki page. It displays a page +containing a form and which provides the following: -After we've defined the routes for our application, we can register views -to handle the processing and rendering that needs to happen when each route is -requested. +- Again, we extend the ``layout.jinja2`` template, which provides the skeleton + of the page (line 1). +- Override the ``subtitle`` block to affect the ``<title>`` tag in the + ``head`` of the page (line 3). +- A 10-row by 60-column ``textarea`` field named ``body`` that is filled with + any existing page data when it is rendered (line 14). +- A submit button that has the name ``form.submitted`` (line 17). +- The form POSTs back to the ``save_url`` argument supplied by the view (line + 12). The view will use the ``body`` and ``form.submitted`` values. -#. Add a declaration which maps the ``view_wiki`` route to the view named - ``view_wiki`` in our ``views.py`` file. This is the :term:`default view` - for the wiki. -#. Add a declaration which maps the ``view_page`` route to the view named - ``view_page`` in our ``views.py`` file. +The ``404.jinja2`` template +--------------------------- -#. Add a declaration which maps the ``add_page`` route to the view named - ``add_page`` in our ``views.py`` file. +Replace ``tutorial/templates/404.jinja2`` with the following content: -#. Add a declaration which maps the ``edit_page`` route to the view named - ``edit_page`` in our ``views.py`` file. +.. literalinclude:: src/views/tutorial/templates/404.jinja2 + :linenos: + :language: html -As a result of our edits, the ``__init__.py`` file should look -something like so: +This template is linked from the ``notfound_view`` defined in +``tutorial/views/notfound.py`` as shown here: -.. literalinclude:: src/views/tutorial/__init__.py +.. literalinclude:: src/views/tutorial/views/notfound.py :linenos: + :emphasize-lines: 6 :language: python -Viewing the Application in a Browser +There are several important things to note about this configuration: + +- The ``notfound_view`` in the above snippet is called an + :term:`exception view`. For more information see + :ref:`special_exceptions_in_callables`. +- The ``notfound_view`` sets the response status to 404. It's possible + to affect the response object used by the renderer via + :ref:`request_response_attr`. +- The ``notfound_view`` is registered as an exception view and will be invoked + **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an + exception. This means it will not be invoked for any responses returned + from a view normally. For example, on line 27 of + ``tutorial/views/default.py`` the exception is raised which will trigger + the view. + +Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template +that was provided by the ``alchemy`` scaffold, as we have created our own +templates for the wiki. + +.. note:: + + Our templates use a ``request`` object that none of our tutorial + views return in their dictionary. ``request`` is one of several names that + are available "by default" in a template when a template renderer is used. + See :ref:`renderer_system_values` for information about other names that + are available by default when a template is used as a renderer. + + +Viewing the application in a browser ==================================== -We can finally examine our application in a browser. The views we'll try are -as follows: +We can finally examine our application in a browser (See +:ref:`wiki2-start-the-application`). Launch a browser and visit +each of the following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` page object. -- Visiting ``http://localhost:6543`` in a browser invokes the - ``view_wiki`` view. This always redirects to the ``view_page`` view - of the FrontPage page object. +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. -- Visiting ``http://localhost:6543/FrontPage`` in a browser invokes - the ``view_page`` view of the front page page object. +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser - invokes the edit view for the front page object. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a page. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. -Try generating an error within the body of a view by adding code to -the top of it that generates an exception (e.g. ``raise -Exception('Forced Exception')``). Then visit the error-raising view -in a browser. You should see an interactive exception handler in the -browser which allows you to examine values in a post-mortem mode. +- To generate an error, visit http://localhost:6543/foobars/edit_page which + will generate a ``NoResultFound: No row was found for one()`` error. You'll + see an interactive traceback facility provided by + :term:`pyramid_debugtoolbar`. +.. _jinja2: http://jinja.pocoo.org/ diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst new file mode 100644 index 000000000..523a6e6d8 --- /dev/null +++ b/docs/tutorials/wiki2/design.rst @@ -0,0 +1,162 @@ +.. _wiki2_design: + +====== +Design +====== + +Following is a quick overview of the design of our wiki application to help us +understand the changes that we will be making as we work through the tutorial. + +Overall +======= + +We choose to use :term:`reStructuredText` markup in the wiki text. Translation +from reStructuredText to HTML is provided by the widely used ``docutils`` +Python module. We will add this module to the dependency list in the project's +``setup.py`` file. + +Models +====== + +We'll be using an SQLite database to hold our wiki data, and we'll be using +:term:`SQLAlchemy` to access the data in this database. + +Within the database, we will define two tables: + +- The `users` table which will store the `id`, `name`, `password_hash` and + `role` of each wiki user. +- The `pages` table, whose elements will store the wiki pages. + There are four columns: `id`, `name`, `data` and `creator_id`. + +There is a one-to-many relationship between `users` and `pages` tracking +the user who created each wiki page defined by the `creator_id` column on the +`pages` table. + +URLs like ``/PageName`` will try to find an element in the `pages` table that +has a corresponding name. + +To add a page to the wiki, a new row is created and the text is stored in +`data`. + +A page named ``FrontPage`` containing the text *This is the front page*, will +be created when the storage is initialized, and will be used as the wiki home +page. + +Wiki Views +========== + +There will be three views to handle the normal operations of adding, editing, +and viewing wiki pages, plus one view for the wiki front page. Two templates +will be used, one for viewing, and one for both adding and editing wiki pages. + +As of version 1.5 :app:`Pyramid` no longer ships with templating systems. In +this tutorial, we will use :term:`Jinja2`. Jinja2 is a modern and +designer-friendly templating language for Python, modeled after Django's +templates. + +Security +======== + +We'll eventually be adding security to our application. To do this, we'll +be using a very simple role-based security model. We'll assign a single +role category to each user in our system. + +`basic` + An authenticated user who can view content and create new pages. A `basic` + user may also edit the pages they have created but not pages created by + other users. + +`editor` + An authenticated user who can create and edit any content in the system. + +In order to accomplish this we'll need to define an authentication policy +which can identify users by their :term:`userid` and role. Then we'll +need to define a page :term:`resource` which contains the appropriate +:term:`ACL`: + ++----------+--------------------+----------------+ +| Action | Principal | Permission | ++==========+====================+================+ +| Allow | Everyone | view | ++----------+--------------------+----------------+ +| Allow | group:basic | create | ++----------+--------------------+----------------+ +| Allow | group:editors | edit | ++----------+--------------------+----------------+ +| Allow | <creator of page> | edit | ++----------+--------------------+----------------+ + +Permission declarations will be added to the views to assert the security +policies as each request is handled. + +On the security side of the application there are two additional views for +handling login and logout as well as two exception views for handling +invalid access attempts and unhandled URLs. + +Summary +======= + +The URL, actions, template, and permission associated to each view are listed +in the following table: + ++----------------------+-----------------------+-------------+----------------+------------+ +| URL | Action | View | Template | Permission | +| | | | | | ++======================+=======================+=============+================+============+ +| / | Redirect to | view_wiki | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName | Display existing | view_page | view.jinja2 | view | +| | page [2]_ | [1]_ | | | +| | | | | | +| | | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName/edit_page | Display edit form | edit_page | edit.jinja2 | edit | +| | with existing | | | | +| | content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, redirect | | | | +| | to /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /add_page/PageName | Create the page | add_page | edit.jinja2 | create | +| | *PageName* in | | | | +| | storage, display | | | | +| | the edit form | | | | +| | without content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | redirect to | | | | +| | /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /login | Display login form, | login | login.jinja2 | | +| | Forbidden [3]_ | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | authenticate. | | | | +| | | | | | +| | - If authentication | | | | +| | succeeds, | | | | +| | redirect to the | | | | +| | page from which | | | | +| | we came. | | | | +| | | | | | +| | - If authentication | | | | +| | fails, display | | | | +| | login form with | | | | +| | "login failed" | | | | +| | message. | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /logout | Redirect to | logout | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ + +.. [1] This is the default view for a Page context when there is no view name. +.. [2] Pyramid will return a default 404 Not Found page if the page *PageName* + does not exist yet. +.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke + a view that is not authorized by the authorization policy. diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index c80b43337..f264448b0 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -1,42 +1,40 @@ +.. _wiki2_distributing_your_application: + ============================= Distributing Your Application ============================= -Once your application works properly, you can create a "tarball" from -it by using the ``setup.py sdist`` command. The following commands -assume your current working directory is the ``tutorial`` package -we've created and that the parent directory of the ``tutorial`` -package is a virtualenv representing a :app:`Pyramid` environment. +Once your application works properly, you can create a "tarball" from it by +using the ``setup.py sdist`` command. The following commands assume your +current working directory contains the ``tutorial`` package and the +``setup.py`` file. On UNIX: -.. code-block:: text +.. code-block:: bash - $ ../bin/python setup.py sdist + $ $VENV/bin/python setup.py sdist On Windows: -.. code-block:: text +.. code-block:: doscon - c:\pyramidtut> ..\Scripts\python setup.py sdist + c:\pyramidtut> %VENV%\Scripts\python setup.py sdist The output of such a command will be something like: .. code-block:: text running sdist - # ... more output ... + # .. more output .. creating dist - tar -cf dist/tutorial-0.1.tar tutorial-0.1 - gzip -f9 dist/tutorial-0.1.tar - removing 'tutorial-0.1' (and everything under it) - -Note that this command creates a tarball in the "dist" subdirectory -named ``tutorial-0.1.tar.gz``. You can send this file to your friends -to show them your cool new application. They should be able to -install it by pointing the ``easy_install`` command directly at it. -Or you can upload it to `PyPI <http://pypi.python.org>`_ and share it -with the rest of the world, where it can be downloaded via -``easy_install`` remotely like any other package people download from -PyPI. - + Creating tar archive + removing 'tutorial-0.0' (and everything under it) + +Note that this command creates a tarball in the "dist" subdirectory named +``tutorial-0.0.tar.gz``. You can send this file to your friends to show them +your cool new application. They should be able to install it by pointing the +``easy_install`` command directly at it. Or you can upload it to `PyPI +<http://pypi.python.org>`_ and share it with the rest of the world, where it +can be downloaded via ``easy_install`` remotely like any other package people +download from PyPI. diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index d05d70f3c..18e9f552e 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,29 +1,29 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL Dispatch Wiki Tutorial +SQLAlchemy + URL dispatch wiki tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch` -based -:app:`Pyramid` application to a developer familiar with Python, and will be -most familiar to developers who have used the :term:`Pylons` 1.X web -framework. When the tutorial is finished, the developer will have created a -basic Wiki application with authentication. +This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based +:app:`Pyramid` application to a developer familiar with Python. When the +tutorial is finished, the developer will have created a basic wiki +application with authentication and authorization. -For cut and paste purposes, the source code for all stages of this -tutorial can be browsed at -`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/ -<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/>`_. +For cut and paste purposes, the source code for all stages of this tutorial can +be browsed on GitHub at `docs/tutorials/wiki2/src +<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src>`_, +which corresponds to the same location if you have Pyramid sources. .. toctree:: :maxdepth: 2 background + design installation basiclayout definingmodels definingviews + authentication authorization tests distributing - diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index bd597b5df..f4676345e 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -1,240 +1,521 @@ +.. _wiki2_installation: + ============ Installation ============ -This tutorial assumes that Python and virtualenv are already installed -and working in your system. If you need help setting this up, you should -refer to the chapters on :ref:`installing_chapter`. +Before you begin +---------------- + +This tutorial assumes that you have already followed the steps in +:ref:`installing_chapter`, except **do not create a virtual environment or +install Pyramid**. Thereby you will satisfy the following requirements. + +* A Python interpreter is installed on your operating system. +* You've satisfied the :ref:`requirements-for-installing-packages`. + + +Create directory to contain the project +--------------------------------------- + +We need a workspace for our project files. + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ mkdir ~/pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\> mkdir pyramidtut + + +Create and use a virtual Python environment +------------------------------------------- + +Next let's create a virtual environment workspace for our project. We will use +the ``VENV`` environment variable instead of the absolute path of the virtual +environment. + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ export VENV=~/pyramidtut + $ python3 -m venv $VENV + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\> set VENV=c:\pyramidtut + +Each version of Python uses different paths, so you will need to adjust the +path to the command for your Python version. + +Python 2.7: + +.. code-block:: doscon + + c:\> c:\Python27\Scripts\virtualenv %VENV% -Preparation -=========== +Python 3.5: -Please take the following steps to prepare for the tutorial. The -steps are slightly different depending on whether you're using UNIX or -Windows. +.. code-block:: doscon -Preparation, UNIX ------------------ + c:\> c:\Python35\Scripts\python -m venv %VENV% -#. Install SQLite3 and its development packages if you don't already - have them installed. Usually this is via your system's package - manager. For example, on a Debian Linux system, do ``sudo apt-get - install libsqlite3-dev``. -#. Use your Python's virtualenv to make a workspace: +Upgrade ``pip`` and ``setuptools`` in the virtual environment +------------------------------------------------------------- - .. code-block:: text +On UNIX +^^^^^^^ - $ path/to/my/Python-2.6/bin/virtualenv --no-site-packages pyramidtut +.. code-block:: bash -#. Switch to the ``pyramidtut`` directory: + $ $VENV/bin/pip install --upgrade pip setuptools - .. code-block:: text +On Windows +^^^^^^^^^^ - $ cd pyramidtut +.. code-block:: doscon -#. Use ``easy_install`` to get :app:`Pyramid` and its direct - dependencies installed: + c:\> %VENV%\Scripts\pip install --upgrade pip setuptools - .. code-block:: text - $ bin/easy_install pyramid +Install Pyramid into the virtual Python environment +--------------------------------------------------- -#. Use ``easy_install`` to install various packages from PyPI. +On UNIX +^^^^^^^ - .. code-block:: text +.. code-block:: bash - $ bin/easy_install docutils nose coverage zope.sqlalchemy \ - SQLAlchemy repoze.tm2 + $ $VENV/bin/pip install pyramid -Preparation, Windows --------------------- +On Windows +^^^^^^^^^^ -#. Use your Python's virtualenv to make a workspace: +.. code-block:: doscon - .. code-block:: text + c:\> %VENV%\Scripts\pip install pyramid - c:\> c:\Python26\Scripts\virtualenv --no-site-packages pyramidtut -#. Switch to the ``pyramidtut`` directory: +Install SQLite3 and its development packages +-------------------------------------------- - .. code-block:: text +If you used a package manager to install your Python or if you compiled +your Python from source, then you must install SQLite3 and its +development packages. If you downloaded your Python as an installer +from https://www.python.org, then you already have it installed and can skip +this step. - c:\> cd pyramidtut +If you need to install the SQLite3 packages, then, for example, using +the Debian system and ``apt-get``, the command would be the following: -#. Use ``easy_install`` to get :app:`Pyramid` and its direct - dependencies installed: +.. code-block:: bash - .. code-block:: text + $ sudo apt-get install libsqlite3-dev - c:\pyramidtut> Scripts\easy_install pyramid -#. Use ``easy_install`` to install various packages from PyPI. +Change directory to your virtual Python environment +--------------------------------------------------- - .. code-block:: text +Change directory to the ``pyramidtut`` directory, which is both your workspace +and your virtual environment. - c:\pyramidtut> Scripts\easy_install docutils ^ - nose coverage zope.sqlalchemy SQLAlchemy repoze.tm2 +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ cd pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\> cd pyramidtut .. _sql_making_a_project: -Making a Project -================ +Making a project +---------------- -Your next step is to create a project. :app:`Pyramid` supplies a -variety of scaffolds to generate sample projects. We will use the -``pyramid_routesalchemy`` scaffold, which generates an application +Your next step is to create a project. For this tutorial we will use +the :term:`scaffold` named ``alchemy`` which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. -The below instructions assume your current working directory is the -"virtualenv" named "pyramidtut". +:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We +will use ``pcreate``, a script that comes with Pyramid, to create our project +using a scaffold. + +By passing ``alchemy`` into the ``pcreate`` command, the script creates the +files needed to use SQLAlchemy. By passing in our application name +``tutorial``, the script inserts that application name into all the required +files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the +``pyramidtut/bin`` directory. -On UNIX: +The below instructions assume your current working directory is "pyramidtut". -.. code-block:: text +On UNIX +^^^^^^^ - $ bin/paster create -t pyramid_routesalchemy tutorial +.. code-block:: bash -On Windows: + $ $VENV/bin/pcreate -s alchemy tutorial -.. code-block:: text +On Windows +^^^^^^^^^^ - c:\pyramidtut> Scripts\paster create -t pyramid_routesalchemy tutorial +.. code-block:: doscon -.. note:: If you are using Windows, the ``pyramid_routesalchemy`` - scaffold may not deal gracefully with installation into a - location that contains spaces in the path. If you experience - startup problems, try putting both the virtualenv and the project - into directories that do not contain spaces in their paths. + c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial -Installing the Project in "Development Mode" -============================================ +.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal + gracefully with installation into a location that contains spaces in the + path. If you experience startup problems, try putting both the virtual + environment and the project into directories that do not contain spaces in + their paths. -In order to do development on the project easily, you must "register" -the project as a development egg in your workspace using the -``setup.py develop`` command. In order to do so, cd to the "tutorial" -directory you created in :ref:`sql_making_a_project`, and run the -"setup.py develop" command using virtualenv Python interpreter. -On UNIX: +.. _installing_project_in_dev_mode: -.. code-block:: text +Installing the project in development mode +------------------------------------------ + +In order to do development on the project easily, you must "register" the +project as a development egg in your workspace using the ``pip install -e .`` +command. In order to do so, change directory to the ``tutorial`` directory that +you created in :ref:`sql_making_a_project`, and run the ``pip install -e .`` +command using the virtual environment Python interpreter. + +On UNIX +^^^^^^^ + +.. code-block:: bash $ cd tutorial - $ ../bin/python setup.py develop + $ $VENV/bin/pip install -e . -On Windows: +On Windows +^^^^^^^^^^ -.. code-block:: text +.. code-block:: doscon c:\pyramidtut> cd tutorial - c:\pyramidtut\tutorial> ..\Scripts\python setup.py develop + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . -.. _sql_running_tests: +The console will show ``pip`` checking for packages and installing missing +packages. Success executing this command will show a line like the following: -Running the Tests -================= +.. code-block:: bash -After you've installed the project in development mode, you may run -the tests for the project. + Successfully installed Chameleon-2.24 Mako-1.0.4 MarkupSafe-0.23 \ + Pygments-2.1.3 SQLAlchemy-1.0.12 pyramid-chameleon-0.3 \ + pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \ + transaction-1.4.4 tutorial waitress-0.8.10 zope.sqlalchemy-0.7.6 -On UNIX: -.. code-block:: text +.. _install-testing-requirements: - $ ../bin/python setup.py test -q +Install testing requirements +---------------------------- -On Windows: +In order to run tests, we need to install the testing requirements. This is +done through our project's ``setup.py`` file, in the ``tests_require`` and +``extras_require`` stanzas, and by issuing the command below for your +operating system. -.. code-block:: text +.. literalinclude:: src/installation/setup.py + :language: python + :linenos: + :lineno-start: 22 + :lines: 22-26 - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q +.. literalinclude:: src/installation/setup.py + :language: python + :linenos: + :lineno-start: 45 + :lines: 45-47 -Starting the Application -======================== +On UNIX +^^^^^^^ -Start the application. +.. code-block:: bash + + $ $VENV/bin/pip install -e ".[testing]" + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]" + + +.. _sql_running_tests: + +Run the tests +------------- + +After you've installed the project in development mode as well as the testing +requirements, you may run the tests for the project. + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py -q + +On Windows +^^^^^^^^^^ -On UNIX: +.. code-block:: doscon -.. code-block:: text + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q - $ ../bin/paster serve development.ini --reload +For a successful test run, you should see output that ends like this: -On Windows: +.. code-block:: bash -.. code-block:: text + .. + 2 passed in 0.44 seconds - c:\pyramidtut\tutorial> ..\Scripts\paster serve development.ini --reload -Exposing Test Coverage Information -================================== +Expose test coverage information +-------------------------------- -You can run the ``nosetests`` command to see test coverage -information. This runs the tests in the same way that ``setup.py -test`` does but provides additional "coverage" information, exposing -which lines of your project are "covered" (or not covered) by the +You can run the ``py.test`` command to see test coverage information. This +runs the tests in the same way that ``py.test`` does, but provides additional +"coverage" information, exposing which lines of your project are covered by the tests. -To get this functionality working, we'll need to install a couple of -other packages into our ``virtualenv``: ``nose`` and ``coverage``: +We've already installed the ``pytest-cov`` package into our virtual +environment, so we can run the tests with coverage. -On UNIX: +On UNIX +^^^^^^^ -.. code-block:: text +.. code-block:: bash - $ ../bin/easy_install nose coverage + $ $VENV/bin/py.test --cov=tutorial --cov-report=term-missing tutorial/tests.py -On Windows: +On Windows +^^^^^^^^^^ -.. code-block:: text +.. code-block:: doscon - c:\pyramidtut\tutorial> ..\Scripts\easy_install nose coverage + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \ + --cov-report=term-missing tutorial\tests.py -Once ``nose`` and ``coverage`` are installed, we can actually run the -coverage tests. +If successful, you will see output something like this: -On UNIX: +.. code-block:: bash -.. code-block:: text + ======================== test session starts ======================== + platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: + plugins: cov-2.2.1 + collected 2 items - $ ../bin/nosetests --cover-package=tutorial --cover-erase --with-coverage + tutorial/tests.py .. + ------------------ coverage: platform Python 3.5.1 ------------------ + Name Stmts Miss Cover Missing + ---------------------------------------------------------------- + tutorial/__init__.py 8 6 25% 7-12 + tutorial/models/__init__.py 22 0 100% + tutorial/models/meta.py 5 0 100% + tutorial/models/mymodel.py 8 0 100% + tutorial/routes.py 3 3 0% 1-3 + tutorial/scripts/__init__.py 0 0 100% + tutorial/scripts/initializedb.py 26 26 0% 1-45 + tutorial/tests.py 39 0 100% + tutorial/views/__init__.py 0 0 100% + tutorial/views/default.py 12 0 100% + tutorial/views/notfound.py 4 4 0% 1-7 + ---------------------------------------------------------------- + TOTAL 127 39 69% -On Windows: + ===================== 2 passed in 0.57 seconds ====================== -.. code-block:: text +Our package doesn't quite have 100% test coverage. - c:\pyramidtut\tutorial> ..\Scripts\nosetests --cover-package=tutorial ^ - --cover-erase --with-coverage -Looks like our package's ``models`` module doesn't quite have 100% -test coverage. +.. _initialize_db_wiki2: -Visit the Application in a Browser -================================== +Initializing the database +------------------------- -In a browser, visit ``http://localhost:6543/``. You will see the -generated application's default page. +We need to use the ``initialize_tutorial_db`` :term:`console script` to +initialize our database. -Decisions the ``pyramid_routesalchemy`` Scaffold Has Made For You -================================================================= +.. note:: + + The ``initialize_tutorial_db`` command does not perform a migration, but + rather it simply creates missing tables and adds some dummy data. If you + already have a database, you should delete it before running + ``initialize_tutorial_db`` again. + +.. note:: + + The ``initialize_tutorial_db`` command is not performing a migration but + rather simply creating missing tables and adding some dummy data. If you + already have a database, you should delete it before running + ``initialize_tutorial_db`` again. + +Type the following command, making sure you are still in the ``tutorial`` +directory (the directory with a ``development.ini`` in it): + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/initialize_tutorial_db development.ini + +On Windows +^^^^^^^^^^ + +.. code-block:: doscon + + c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini + +The output to your console should be something like this: + +.. code-block:: bash + + 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE models ( + id INTEGER NOT NULL, + name TEXT, + value INTEGER, + CONSTRAINT pk_models PRIMARY KEY (id) + ) + + + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,804 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) + 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,806 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 00:53:37,807 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) + 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) + 2016-04-09 00:53:37,809 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + +Success! You should now have a ``tutorial.sqlite`` file in your current +working directory. This is an SQLite database with a single table defined in it +(``models``). + +.. _wiki2-start-the-application: + +Start the application +--------------------- + +Start the application. + +On UNIX +^^^^^^^ + +.. code-block:: bash -Creating a project using the ``pyramid_routesalchemy`` scaffold makes -the following assumptions: + $ $VENV/bin/pserve development.ini --reload -- you are willing to use :term:`SQLAlchemy` as a database access tool +On Windows +^^^^^^^^^^ -- you are willing to use :term:`url dispatch` to map URLs to code. +.. code-block:: doscon -- you want to configure your application *imperatively* (no - :term:`declarative configuration` such as ZCML). + c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload .. note:: - :app:`Pyramid` supports any persistent storage mechanism (e.g. object - database or filesystem files, etc). It also supports an additional - mechanism to map URLs to code (:term:`traversal`). However, for the - purposes of this tutorial, we'll only be using url dispatch and - SQLAlchemy. + Your OS firewall, if any, may pop up a dialog asking for authorization + to allow python to accept incoming network connections. + +If successful, you will see something like this on your console:: + + Starting subprocess with file monitor + Starting server in PID 82349. + serving on http://127.0.0.1:6543 + +This means the server is ready to accept requests. + + +Visit the application in a browser +---------------------------------- + +In a browser, visit http://localhost:6543/. You will see the generated +application's default page. + +One thing you'll notice is the "debug toolbar" icon on right hand side of the +page. You can read more about the purpose of the icon at +:ref:`debug_toolbar`. It allows you to get information about your +application while you develop. + + +Decisions the ``alchemy`` scaffold has made for you +--------------------------------------------------- + +Creating a project using the ``alchemy`` scaffold makes the following +assumptions: + +- You are willing to use :term:`SQLAlchemy` as a database access tool. + +- You are willing to use :term:`URL dispatch` to map URLs to code. + +- You want to use zope.sqlalchemy_, pyramid_tm_, and the transaction_ packages + to scope sessions to requests. + +- You want to use pyramid_jinja2_ to render your templates. Different + templating engines can be used, but we had to choose one to make this + tutorial. See :ref:`available_template_system_bindings` for some options. + +.. note:: + + :app:`Pyramid` supports any persistent storage mechanism (e.g., object + database or filesystem files). It also supports an additional mechanism to + map URLs to code (:term:`traversal`). However, for the purposes of this + tutorial, we'll only be using URL dispatch and SQLAlchemy. + +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _pyramid_tm: + http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ + +.. _zope.sqlalchemy: + https://pypi.python.org/pypi/zope.sqlalchemy + +.. _transaction: + http://zodb.readthedocs.org/en/latest/transactions.html + +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _pyramid_tm: + http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ + +.. _zope.sqlalchemy: + https://pypi.python.org/pypi/zope.sqlalchemy +.. _transaction: + http://zodb.readthedocs.org/en/latest/transactions.html diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt new file mode 100644 index 000000000..5b0101e5f --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini new file mode 100644 index 000000000..4a6c9325c --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/development.ini @@ -0,0 +1,73 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = seekrit + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini new file mode 100644 index 000000000..a13a0ca19 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/production.ini @@ -0,0 +1,62 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = real-seekrit + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py new file mode 100644 index 000000000..def3ce1f6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/setup.py @@ -0,0 +1,57 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'bcrypt', + 'docutils', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py new file mode 100644 index 000000000..f5c033b8b --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py @@ -0,0 +1,13 @@ +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.include('.security') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py new file mode 100644 index 000000000..6bd3315d6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py new file mode 100644 index 000000000..cb747244f --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py @@ -0,0 +1,8 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py new file mode 100644 index 000000000..8ea3858d2 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -0,0 +1,27 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +<p> +Editing <strong>{{pagename}}</strong> +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +<form action="{{ save_url }}" method="post"> +<div class="form-group"> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + {% if request.user is none %} + <p class="pull-right"> + <a href="{{ request.route_url('login') }}">Login</a> + </p> + {% else %} + <p class="pull-right"> + {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a> + </p> + {% endif %} + {% block content %}{% endblock %} + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..1806de0ff --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 @@ -0,0 +1,26 @@ +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +<p> +<strong> + Login +</strong><br> +{{ message }} +</p> +<form action="{{ url }}" method="post"> +<input type="hidden" name="next" value="{{ next_url }}"> +<div class="form-group"> + <label for="login">Username</label> + <input type="text" name="login" value="{{ login }}"> +</div> +<div class="form-group"> + <label for="password">Password</label> + <input type="password" name="password"> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +<p>{{ content|safe }}</p> +<p> +<a href="{{ edit_url }}"> + Edit this page +</a> +</p> +<p> + Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>. +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py new file mode 100644 index 000000000..99e95efd3 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py new file mode 100644 index 000000000..2b993b430 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -0,0 +1,46 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..models import User + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login(request): + next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') + message = '' + login = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + next_url=next_url, + login=login, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'next': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py new file mode 100644 index 000000000..1b071434c --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -0,0 +1,79 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +from ..models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound('No such page') + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + user = request.user + if user is None or (user.role != 'editor' and page.creator != user): + raise HTTPForbidden + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + user = request.user + if user is None or user.role not in ('editor', 'basic'): + raise HTTPForbidden + pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = request.user + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in +++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt index d41f7f90f..5b0101e5f 100644 --- a/docs/tutorials/wiki2/src/authorization/README.txt +++ b/docs/tutorials/wiki2/src/authorization/README.txt @@ -1,4 +1,14 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 3b615f635..4a6c9325c 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -1,32 +1,44 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db - -[pipeline:main] -pipeline = - egg:WebError#evalerror - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = seekrit + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -38,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = @@ -53,6 +70,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 0fdc38811..a13a0ca19 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -1,43 +1,30 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite -[pipeline:main] -pipeline = - weberror - tm - tutorial +auth.secret = real-seekrit [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -72,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/setup.cfg b/docs/tutorials/wiki2/src/authorization/setup.cfg deleted file mode 100644 index 23b2ad983..000000000 --- a/docs/tutorials/wiki2/src/authorization/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index ae9869d50..def3ce1f6 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -1,35 +1,42 @@ import os -import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'repoze.tm2>=1.0b1', # default_commit_veto 'zope.sqlalchemy', - 'WebError', - 'docutils', + 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -37,12 +44,14 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', - install_requires = requires, - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 05183d3d4..f5c033b8b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -1,45 +1,13 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy -from sqlalchemy import engine_from_config - -from tutorial.models import initialize_sql -from tutorial.security import groupfinder def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) - authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder) - authz_policy = ACLAuthorizationPolicy() - config = Configurator(settings=settings, - root_factory='tutorial.models.RootFactory', - authentication_policy=authn_policy, - authorization_policy=authz_policy) - config.add_static_view('static', 'tutorial:static') - - config.add_route('view_wiki', '/') - config.add_route('login', '/login') - config.add_route('logout', '/logout') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') - - config.add_view('tutorial.views.view_wiki', route_name='view_wiki') - config.add_view('tutorial.login.login', route_name='login', - renderer='tutorial:templates/login.pt') - config.add_view('tutorial.login.logout', route_name='logout') - config.add_view('tutorial.views.view_page', route_name='view_page', - renderer='tutorial:templates/view.pt') - config.add_view('tutorial.views.add_page', route_name='add_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.views.edit_page', route_name='edit_page', - renderer='tutorial:templates/edit.pt', permission='edit') - config.add_view('tutorial.login.login', - context='pyramid.exceptions.Forbidden', - renderer='tutorial:templates/login.pt') + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.include('.security') + config.scan() return config.make_wsgi_app() - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py deleted file mode 100644 index 7a1d1f663..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/login.py +++ /dev/null @@ -1,38 +0,0 @@ -from pyramid.httpexceptions import HTTPFound -from pyramid.security import remember -from pyramid.security import forget -from pyramid.url import route_url - -from tutorial.security import USERS - -def login(request): - login_url = route_url('login', request) - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -def logout(request): - headers = forget(request) - return HTTPFound(location = route_url('view_wiki', request), - headers = headers) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py deleted file mode 100644 index 53c6d1122..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py +++ /dev/null @@ -1,50 +0,0 @@ -import transaction - -from pyramid.security import Allow -from pyramid.security import Everyone - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - def __init__(self, name, data): - self.name = name - self.data = data - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'This is the front page') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - pass - -class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py new file mode 100644 index 000000000..6bd3315d6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py new file mode 100644 index 000000000..f0a8b7f96 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -0,0 +1,56 @@ +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py index cfd13071e..25cff7b05 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -1,8 +1,40 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import ( + Authenticated, + Everyone, +) -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) +from .models import User + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + + def effective_principals(self, request): + principals = [Everyone] + user = request.user + if user is not None: + principals.append(Authenticated) + principals.append(str(user.id)) + principals.append('role:' + user.role) + return principals + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +<p> +Editing <strong>{{pagename}}</strong> +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +<form action="{{ save_url }}" method="post"> +<div class="form-group"> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt deleted file mode 100644 index ca28b9fa5..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt +++ /dev/null @@ -1,62 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> - </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Editing <b><span tal:replace="page.name">Page Name - Goes Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"> - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> - </div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <form action="${save_url}" method="post"> - <textarea name="body" tal:content="page.data" rows="10" - cols="60"/><br/> - <input type="submit" name="form.submitted" value="Save"/> - </form> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + {% if request.user is none %} + <p class="pull-right"> + <a href="{{ request.route_url('login') }}">Login</a> + </p> + {% else %} + <p class="pull-right"> + {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a> + </p> + {% endif %} + {% block content %}{% endblock %} + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..1806de0ff --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -0,0 +1,26 @@ +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +<p> +<strong> + Login +</strong><br> +{{ message }} +</p> +<form action="{{ url }}" method="post"> +<input type="hidden" name="next" value="{{ next_url }}"> +<div class="form-group"> + <label for="login">Username</label> + <input type="text" name="login" value="{{ login }}"> +</div> +<div class="form-group"> + <label for="password">Password</label> + <input type="password" name="password"> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt deleted file mode 100644 index 64e592ea9..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt +++ /dev/null @@ -1,58 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>Login - Pyramid tutorial wiki (based on TurboGears - 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> - </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - <b>Login</b><br/> - <span tal:replace="message"/> - </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <form action="${url}" method="post"> - <input type="hidden" name="came_from" value="${came_from}"/> - <input type="text" name="login" value="${login}"/><br/> - <input type="password" name="password" - value="${password}"/><br/> - <input type="submit" name="form.submitted" value="Log In"/> - </form> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt deleted file mode 100644 index d98420680..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,75 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> - </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> - </ul> - </div> - </div> - </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +<p>{{ content|safe }}</p> +<p> +<a href="{{ edit_url }}"> + Edit this page +</a> +</p> +<p> + Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>. +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt deleted file mode 100644 index 5a69818c1..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt +++ /dev/null @@ -1,65 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> - </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Viewing <b><span tal:replace="page.name">Page Name - Goes Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"> - <span tal:condition="logged_in"> - <a href="${request.application_url}/logout">Logout</a> - </span> - </div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div tal:replace="structure content"> - Page text goes here. - </div> - <p> - <a tal:attributes="href edit_url" href=""> - Edit this page - </a> - </p> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index 332031ba4..99e95efd3 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -1,136 +1,65 @@ import unittest +import transaction from pyramid import testing -def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - def tearDown(self): - testing.tearDown() +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) - - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - def tearDown(self): - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '<div class="document">\n' - '<p>Hello <a href="http://example.com/add_page/CruelWorld">' - 'CruelWorld</a> ' - '<a href="http://example.com/IDoExist">' - 'IDoExist</a>' - '</p>\n</div>\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): +class BaseTest(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py deleted file mode 100644 index e0b84971d..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ /dev/null @@ -1,72 +0,0 @@ -import re - -from docutils.core import publish_parts - -from pyramid.httpexceptions import HTTPFound, HTTPNotFound -from pyramid.security import authenticated_userid -from pyramid.url import route_url - -from tutorial.models import DBSession -from tutorial.models import Page - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -def view_wiki(request): - return HTTPFound(location = route_url('view_page', request, - pagename='FrontPage')) - -def view_page(request): - pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') - - def check(match): - word = match.group(1) - exists = session.query(Page).filter_by(name=word).all() - if exists: - view_url = route_url('view_page', request, pagename=word) - return '<a href="%s">%s</a>' % (view_url, word) - else: - add_url = route_url('add_page', request, pagename=word) - return '<a href="%s">%s</a>' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = route_url('edit_page', request, pagename=pagename) - logged_in = authenticated_userid(request) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=logged_in) - -def add_page(request): - name = request.matchdict['pagename'] - if 'form.submitted' in request.params: - session = DBSession() - body = request.params['body'] - page = Page(name, body) - session.add(page) - return HTTPFound(location = route_url('view_page', request, - pagename=name)) - save_url = route_url('add_page', request, pagename=name) - page = Page('', '') - logged_in = authenticated_userid(request) - return dict(page=page, save_url=save_url, logged_in=logged_in) - -def edit_page(request): - name = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=name).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - session.add(page) - return HTTPFound(location = route_url('view_page', request, - pagename=name)) - - logged_in = authenticated_userid(request) - return dict( - page=page, - save_url = route_url('edit_page', request, pagename=name), - logged_in = logged_in, - ) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py new file mode 100644 index 000000000..2b993b430 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -0,0 +1,46 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..models import User + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login(request): + next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') + message = '' + login = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + next_url=next_url, + login=login, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'next': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py new file mode 100644 index 000000000..9358993ea --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -0,0 +1,64 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from ..models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2', + permission='view') +def view_page(request): + page = request.context.page + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', + permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', + permission='create') +def add_page(request): + pagename = request.context.pagename + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = request.user + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in +++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt index d41f7f90f..5b0101e5f 100644 --- a/docs/tutorials/wiki2/src/basiclayout/README.txt +++ b/docs/tutorials/wiki2/src/basiclayout/README.txt @@ -1,4 +1,14 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index 3b615f635..22b733e10 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -1,32 +1,42 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db - -[pipeline:main] -pipeline = - egg:WebError#evalerror - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -38,6 +48,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = @@ -53,6 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index 0fdc38811..d2ecfe22a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -1,43 +1,28 @@ -[app:tutorial] -use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +[app:main] +use = egg:tutorial -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en -[pipeline:main] -pipeline = - weberror - tm - tutorial +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -72,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.cfg b/docs/tutorials/wiki2/src/basiclayout/setup.cfg deleted file mode 100644 index 23b2ad983..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index eaf1ddcfe..ede0a82ef 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -1,34 +1,40 @@ import os -import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'repoze.tm2>=1.0b1', # default_commit_veto 'zope.sqlalchemy', - 'WebError', + 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -36,12 +42,14 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', - install_requires = requires, - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index c74f07652..4dab44823 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -1,18 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static') - config.add_route('home', '/') - config.add_view('tutorial.views.my_view', route_name='home', - renderer='templates/mytemplate.pt') + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.scan() return config.make_wsgi_app() - - diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py deleted file mode 100644 index 4fd010c5c..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models.py +++ /dev/null @@ -1,43 +0,0 @@ -import transaction - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Unicode - -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker( - extension=ZopeTransactionExtension())) -Base = declarative_base() - -class MyModel(Base): - __tablename__ = 'models' - id = Column(Integer, primary_key=True) - name = Column(Unicode(255), unique=True) - value = Column(Integer) - - def __init__(self, name, value): - self.name = name - self.value = value - -def populate(): - session = DBSession() - model = MyModel(name=u'root',value=55) - session.add(model) - session.flush() - transaction.commit() - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - populate() - except IntegrityError: - pass diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py new file mode 100644 index 000000000..48a957ecb --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -0,0 +1,73 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py new file mode 100644 index 000000000..d65a01a42 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -0,0 +1,18 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + +from .meta import Base + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..7307ecc5c --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -0,0 +1,45 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ab8c5ea3d --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>Alchemy Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + {% block content %} + <p>No content</p> + {% endblock content %} + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..6b49869c4 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt deleted file mode 100644 index d98420680..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,75 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> - </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> - </ul> - </div> - </div> - </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py index 5efa6affa..99e95efd3 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py @@ -1,23 +1,65 @@ import unittest +import transaction + from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import initialize_sql - session = initialize_sql(create_engine('sqlite://')) - return session -class TestMyView(unittest.TestCase): +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + - def test_it(self): - from tutorial.views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['root'].name, 'root') +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py deleted file mode 100644 index e550e3257..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from tutorial.models import DBSession -from tutorial.models import MyModel - -def my_view(request): - dbsession = DBSession() - root = dbsession.query(MyModel).filter(MyModel.name==u'root').first() - return {'root':root, 'project':'tutorial'} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py new file mode 100644 index 000000000..ad0c728d7 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py @@ -0,0 +1,33 @@ +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': 'tutorial'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/installation/CHANGES.txt b/docs/tutorials/wiki2/src/installation/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/installation/MANIFEST.in b/docs/tutorials/wiki2/src/installation/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt new file mode 100644 index 000000000..5b0101e5f --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/installation/development.ini b/docs/tutorials/wiki2/src/installation/development.ini new file mode 100644 index 000000000..22b733e10 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/development.ini @@ -0,0 +1,71 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/installation/production.ini b/docs/tutorials/wiki2/src/installation/production.ini new file mode 100644 index 000000000..d2ecfe22a --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/production.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py new file mode 100644 index 000000000..ede0a82ef --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/setup.py @@ -0,0 +1,55 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py new file mode 100644 index 000000000..4dab44823 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py @@ -0,0 +1,12 @@ +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() diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py new file mode 100644 index 000000000..48a957ecb --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py @@ -0,0 +1,73 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py new file mode 100644 index 000000000..d65a01a42 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py @@ -0,0 +1,18 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + +from .meta import Base + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/routes.py b/docs/tutorials/wiki2/src/installation/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..7307ecc5c --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py @@ -0,0 +1,45 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ab8c5ea3d --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>Alchemy Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + {% block content %} + <p>No content</p> + {% endblock content %} + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..6b49869c4 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/tests.py b/docs/tutorials/wiki2/src/installation/tutorial/tests.py new file mode 100644 index 000000000..99e95efd3 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py new file mode 100644 index 000000000..ad0c728d7 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py @@ -0,0 +1,33 @@ +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': 'tutorial'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/models/MANIFEST.in +++ b/docs/tutorials/wiki2/src/models/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt index d41f7f90f..5b0101e5f 100644 --- a/docs/tutorials/wiki2/src/models/README.txt +++ b/docs/tutorials/wiki2/src/models/README.txt @@ -1,4 +1,14 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index 3b615f635..22b733e10 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -1,32 +1,42 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db - -[pipeline:main] -pipeline = - egg:WebError#evalerror - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -38,6 +48,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = @@ -53,6 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index 0fdc38811..d2ecfe22a 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -1,43 +1,28 @@ -[app:tutorial] -use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +[app:main] +use = egg:tutorial -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en -[pipeline:main] -pipeline = - weberror - tm - tutorial +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -72,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/models/setup.cfg b/docs/tutorials/wiki2/src/models/setup.cfg deleted file mode 100644 index 23b2ad983..000000000 --- a/docs/tutorials/wiki2/src/models/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index eaf1ddcfe..742a7c59c 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -1,34 +1,41 @@ import os -import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ + 'bcrypt', 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'repoze.tm2>=1.0b1', # default_commit_veto 'zope.sqlalchemy', - 'WebError', + 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -36,12 +43,14 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', - install_requires = requires, - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index ecc41ca9f..4dab44823 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -1,16 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static') - config.add_route('home', '/') - config.add_view('tutorial.views.my_view', route_name='home', - renderer='templates/mytemplate.pt') + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models.py b/docs/tutorials/wiki2/src/models/tutorial/models.py deleted file mode 100644 index ecc8d567b..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/models.py +++ /dev/null @@ -1,42 +0,0 @@ -import transaction - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker( - extension=ZopeTransactionExtension())) -Base = declarative_base() - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - def __init__(self, name, data): - self.name = name - self.data = data - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'This is the front page') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - pass diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py new file mode 100644 index 000000000..6bd3315d6 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/models/tutorial/routes.py b/docs/tutorials/wiki2/src/models/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/models/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/models/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/models/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki2/src/models/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/theme.css b/docs/tutorials/wiki2/src/models/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/models/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/models/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ab8c5ea3d --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>Alchemy Scaffold for The Pyramid Web Framework</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + {% block content %} + <p>No content</p> + {% endblock content %} + </div> + </div> + <div class="row"> + <div class="links"> + <ul> + <li class="current-version">Generated by v1.7</li> + <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/">Docs</a></li> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="irc://irc.freenode.net#pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="http://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..6b49869c4 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">{{project}}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework 1.7</span>.</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt deleted file mode 100644 index d98420680..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,75 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>The Pyramid Web Application Development Framework</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" href="${request.static_url('tutorial:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> - <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton|Nobile:regular,i,b,bi&subset=latin" type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" href="${request.static_url('tutorial:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top"> - <div class="top align-center"> - <div><img src="${request.static_url('tutorial:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> - </div> - </div> - <div id="middle"> - <div class="middle align-center"> - <p class="app-welcome"> - Welcome to <span class="app-name">${project}</span>, an application generated by<br/> - the Pyramid web application development framework. - </p> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div id="left" class="align-right"> - <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/dev/search.html"> - <input type="text" id="q" name="q" value="" /> - <input type="submit" id="x" value="Go" /> - </form> - </div> - <div id="right" class="align-left"> - <h2>Pyramid links</h2> - <ul class="links"> - <li> - <a href="http://pylonsproject.org">Pylons Website</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#narrative-documentation">Narrative Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#api-documentation">API Documentation</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#tutorials">Tutorials</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#change-history">Change History</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#sample-applications">Sample Applications</a> - </li> - <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/dev/#support-and-development">Support and Development</a> - </li> - <li> - <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> - </li> - </ul> - </div> - </div> - </div> - </div> - <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py index 71f5e21e3..99e95efd3 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py @@ -1,22 +1,65 @@ import unittest +import transaction + from pyramid import testing -def _initTestingDB(): - from tutorial.models import initialize_sql - session = initialize_sql('sqlite://') - return session -class TestMyView(unittest.TestCase): +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): + from .models.meta import Base + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + - def test_it(self): - from tutorial.views import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['root'].name, 'root') +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views.py deleted file mode 100644 index e550e3257..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from tutorial.models import DBSession -from tutorial.models import MyModel - -def my_view(request): - dbsession = DBSession() - root = dbsession.query(MyModel).filter(MyModel.name==u'root').first() - return {'root':root, 'project':'tutorial'} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py new file mode 100644 index 000000000..ad0c728d7 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py @@ -0,0 +1,33 @@ +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': 'tutorial'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/tests/CHANGES.txt b/docs/tutorials/wiki2/src/tests/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt new file mode 100644 index 000000000..5b0101e5f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini new file mode 100644 index 000000000..4a6c9325c --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -0,0 +1,73 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = seekrit + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini new file mode 100644 index 000000000..a13a0ca19 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -0,0 +1,62 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = real-seekrit + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py new file mode 100644 index 000000000..def3ce1f6 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -0,0 +1,57 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'bcrypt', + 'docutils', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py new file mode 100644 index 000000000..f5c033b8b --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -0,0 +1,13 @@ +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.include('.security') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py new file mode 100644 index 000000000..6bd3315d6 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py new file mode 100644 index 000000000..f0a8b7f96 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -0,0 +1,56 @@ +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py new file mode 100644 index 000000000..25cff7b05 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -0,0 +1,40 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import ( + Authenticated, + Everyone, +) + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + + def effective_principals(self, request): + principals = [Everyone] + user = request.user + if user is not None: + principals.append(Authenticated) + principals.append(str(user.id)) + principals.append('role:' + user.role) + return principals + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +<p> +Editing <strong>{{pagename}}</strong> +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +<form action="{{ save_url }}" method="post"> +<div class="form-group"> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + {% if request.user is none %} + <p class="pull-right"> + <a href="{{ request.route_url('login') }}">Login</a> + </p> + {% else %} + <p class="pull-right"> + {{request.user.name}} <a href="{{request.route_url('logout')}}">Logout</a> + </p> + {% endif %} + {% block content %}{% endblock %} + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..1806de0ff --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 @@ -0,0 +1,26 @@ +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +<p> +<strong> + Login +</strong><br> +{{ message }} +</p> +<form action="{{ url }}" method="post"> +<input type="hidden" name="next" value="{{ next_url }}"> +<div class="form-group"> + <label for="login">Username</label> + <input type="text" name="login" value="{{ login }}"> +</div> +<div class="form-group"> + <label for="password">Password</label> + <input type="password" name="password"> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +<p>{{ content|safe }}</p> +<p> +<a href="{{ edit_url }}"> + Edit this page +</a> +</p> +<p> + Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>. +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py deleted file mode 100644 index 98a4969e9..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ /dev/null @@ -1,266 +0,0 @@ -import unittest - -from pyramid import testing - - -def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base - from sqlalchemy import create_engine - engine = create_engine('sqlite:///:memory:') - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - - -class PageModelTests(unittest.TestCase): - - def setUp(self): - self.session = _initTestingDB() - - def tearDown(self): - self.session.remove() - - def _getTargetClass(self): - from tutorial.models import Page - return Page - - def _makeOne(self, name='SomeName', data='some data'): - return self._getTargetClass()(name, data) - - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.name, 'SomeName') - self.assertEqual(instance.data, 'some data') - -class InitializeSqlTests(unittest.TestCase): - - def setUp(self): - from tutorial.models import DBSession - DBSession.remove() - - def tearDown(self): - from tutorial.models import DBSession - DBSession.remove() - - def _callFUT(self, engine): - from tutorial.models import initialize_sql - return initialize_sql(engine) - - def test_it(self): - from sqlalchemy import create_engine - engine = create_engine('sqlite:///:memory:') - self._callFUT(engine) - from tutorial.models import DBSession, Page - self.assertEqual(DBSession.query(Page).one().data, - 'This is the front page') - -class ViewWikiTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) - - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '<div class="document">\n' - '<p>Hello <a href="http://example.com/add_page/CruelWorld">' - 'CruelWorld</a> ' - '<a href="http://example.com/IDoExist">' - 'IDoExist</a>' - '</p>\n</div>\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - self.config.begin() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') - -class FunctionalTests(unittest.TestCase): - - viewer_login = '/login?login=viewer&password=viewer' \ - '&came_from=FrontPage&form.submitted=Login' - viewer_wrong_login = '/login?login=viewer&password=incorrect' \ - '&came_from=FrontPage&form.submitted=Login' - editor_login = '/login?login=editor&password=editor' \ - '&came_from=FrontPage&form.submitted=Login' - - def setUp(self): - from tutorial import main - settings = { 'sqlalchemy.url': 'sqlite:///:memory:'} - app = main({}, **settings) - from webtest import TestApp - self.testapp = TestApp(app) - - def tearDown(self): - del self.testapp - from tutorial.models import DBSession - DBSession.remove() - - def test_root(self): - res = self.testapp.get('/', status=302) - self.assertTrue(not res.body) - - def test_FrontPage(self): - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) - - def test_unexisting_page(self): - res = self.testapp.get('/SomePage', status=404) - - def test_successful_log_in(self): - res = self.testapp.get(self.viewer_login, status=302) - self.assertTrue(res.location == 'FrontPage') - - def test_failed_log_in(self): - res = self.testapp.get(self.viewer_wrong_login, status=200) - self.assertTrue('login' in res.body) - - def test_logout_link_present_when_logged_in(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('Logout' in res.body) - - def test_logout_link_not_present_after_logged_out(self): - self.testapp.get(self.viewer_login, status=302) - self.testapp.get('/FrontPage', status=200) - res = self.testapp.get('/logout', status=302) - self.assertTrue('Logout' not in res.body) - - def test_anonymous_user_cannot_edit(self): - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) - - def test_anonymous_user_cannot_add(self): - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) - - def test_viewer_user_cannot_edit(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) - - def test_viewer_user_cannot_add(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) - - def test_editors_member_user_can_edit(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Editing' in res.body) - - def test_editors_member_user_can_add(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Editing' in res.body) - - def test_editors_member_user_can_view(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py new file mode 100644 index 000000000..715768b2e --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -0,0 +1,122 @@ +import transaction +import unittest +import webtest + + +class FunctionalTests(unittest.TestCase): + + basic_login = ( + '/login?login=basic&password=basic' + '&next=FrontPage&form.submitted=Login') + basic_wrong_login = ( + '/login?login=basic&password=incorrect' + '&next=FrontPage&form.submitted=Login') + editor_login = ( + '/login?login=editor&password=editor' + '&next=FrontPage&form.submitted=Login') + + @classmethod + def setUpClass(cls): + from tutorial.models.meta import Base + from tutorial.models import ( + User, + Page, + get_tm_session, + ) + from tutorial import main + + settings = { + 'sqlalchemy.url': 'sqlite://', + 'auth.secret': 'seekrit', + } + app = main({}, **settings) + cls.testapp = webtest.TestApp(app) + + session_factory = app.registry['dbsession_factory'] + cls.engine = session_factory.kw['bind'] + Base.metadata.create_all(bind=cls.engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + editor = User(name='editor', role='editor') + editor.set_password('editor') + basic = User(name='basic', role='basic') + basic.set_password('basic') + page1 = Page(name='FrontPage', data='This is the front page') + page1.creator = editor + page2 = Page(name='BackPage', data='This is the back page') + page2.creator = basic + dbsession.add_all([basic, editor, page1, page2]) + + @classmethod + def tearDownClass(cls): + from tutorial.models.meta import Base + Base.metadata.drop_all(bind=cls.engine) + + def test_root(self): + res = self.testapp.get('/', status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_FrontPage(self): + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) + + def test_unexisting_page(self): + self.testapp.get('/SomePage', status=404) + + def test_successful_log_in(self): + res = self.testapp.get(self.basic_login, status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_failed_log_in(self): + res = self.testapp.get(self.basic_wrong_login, status=200) + self.assertTrue(b'login' in res.body) + + def test_logout_link_present_when_logged_in(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'Logout' in res.body) + + def test_logout_link_not_present_after_logged_out(self): + self.testapp.get(self.basic_login, status=302) + self.testapp.get('/FrontPage', status=200) + res = self.testapp.get('/logout', status=302) + self.assertTrue(b'Logout' not in res.body) + + def test_anonymous_user_cannot_edit(self): + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() + self.assertTrue(b'Login' in res.body) + + def test_anonymous_user_cannot_add(self): + res = self.testapp.get('/add_page/NewPage', status=302).follow() + self.assertTrue(b'Login' in res.body) + + def test_basic_user_cannot_edit_front(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() + self.assertTrue(b'Login' in res.body) + + def test_basic_user_can_edit_back(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/BackPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_basic_user_can_add(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_edit(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_add(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_view(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py new file mode 100644 index 000000000..2c945ab33 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -0,0 +1,168 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + from ..models import get_tm_session + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('..models') + self.config.include('..routes') + + session_factory = self.config.registry['dbsession_factory'] + self.session = get_tm_session(session_factory, transaction.manager) + + self.init_database() + + def init_database(self): + from ..models.meta import Base + session_factory = self.config.registry['dbsession_factory'] + engine = session_factory.kw['bind'] + Base.metadata.create_all(engine) + + def tearDown(self): + testing.tearDown() + transaction.abort() + + def makeUser(self, name, role, password='dummy'): + from ..models import User + user = User(name=name, role=role) + user.set_password(password) + return user + + def makePage(self, name, data, creator): + from ..models import Page + return Page(name=name, data=data, creator=creator) + + +class ViewWikiTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + self.config.include('..routes') + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import view_wiki + return view_wiki(request) + + def test_it(self): + request = testing.DummyRequest() + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + + +class ViewPageTests(BaseTest): + def _callFUT(self, request): + from tutorial.views.default import view_page + return view_page(request) + + def test_it(self): + from ..routes import PageResource + + # add a page to the db + user = self.makeUser('foo', 'editor') + page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user) + self.session.add_all([page, user]) + + # create a request asking for the page we've created + request = dummy_request(self.session) + request.context = PageResource(page) + + # call the view we're testing and check its behavior + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual( + info['content'], + '<div class="document">\n' + '<p>Hello <a href="http://example.com/add_page/CruelWorld">' + 'CruelWorld</a> ' + '<a href="http://example.com/IDoExist">' + 'IDoExist</a>' + '</p>\n</div>\n') + self.assertEqual(info['edit_url'], + 'http://example.com/IDoExist/edit_page') + + +class AddPageTests(BaseTest): + def _callFUT(self, request): + from tutorial.views.default import add_page + return add_page(request) + + def test_it_pageexists(self): + from ..models import Page + from ..routes import NewPage + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') + self._callFUT(request) + pagecount = self.session.query(Page).filter_by(name='AnotherPage').count() + self.assertGreater(pagecount, 0) + + def test_it_notsubmitted(self): + from ..routes import NewPage + request = dummy_request(self.session) + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') + info = self._callFUT(request) + self.assertEqual(info['pagedata'], '') + self.assertEqual(info['save_url'], + 'http://example.com/add_page/AnotherPage') + + def test_it_submitted(self): + from ..models import Page + from ..routes import NewPage + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') + self._callFUT(request) + page = self.session.query(Page).filter_by(name='AnotherPage').one() + self.assertEqual(page.data, 'Hello yo!') + + +class EditPageTests(BaseTest): + def _callFUT(self, request): + from tutorial.views.default import edit_page + return edit_page(request) + + def makeContext(self, page): + from ..routes import PageResource + return PageResource(page) + + def test_it_notsubmitted(self): + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + + request = dummy_request(self.session) + request.context = self.makeContext(page) + info = self._callFUT(request) + self.assertEqual(info['pagename'], 'abc') + self.assertEqual(info['save_url'], + 'http://example.com/abc/edit_page') + + def test_it_submitted(self): + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.context = self.makeContext(page) + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/abc') + self.assertEqual(page.data, 'Hello yo!') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py new file mode 100644 index 000000000..2b993b430 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -0,0 +1,46 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..models import User + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login(request): + next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') + message = '' + login = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + next_url=next_url, + login=login, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'next': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py new file mode 100644 index 000000000..9358993ea --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -0,0 +1,64 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from ..models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2', + permission='view') +def view_page(request): + page = request.context.page + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', + permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', + permission='create') +def add_page(request): + pagename = request.context.pagename + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = request.user + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/views/MANIFEST.in +++ b/docs/tutorials/wiki2/src/views/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt index d41f7f90f..5b0101e5f 100644 --- a/docs/tutorials/wiki2/src/views/README.txt +++ b/docs/tutorials/wiki2/src/views/README.txt @@ -1,4 +1,14 @@ tutorial README +================== +Getting Started +--------------- +- cd <directory containing this file> + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_tutorial_db development.ini + +- $VENV/bin/pserve development.ini diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index 3b615f635..22b733e10 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -1,32 +1,42 @@ -[app:tutorial] +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] use = egg:tutorial -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db - -[pipeline:main] -pipeline = - egg:WebError#evalerror - tm - tutorial - -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### [server:main] -use = egg:Paste#http -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] -keys = root, sqlalchemy +keys = root, tutorial, sqlalchemy [handlers] keys = console @@ -38,6 +48,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [logger_sqlalchemy] level = INFO handlers = @@ -53,6 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index 0fdc38811..d2ecfe22a 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -1,43 +1,28 @@ -[app:tutorial] -use = egg:tutorial -reload_templates = false -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = false -default_locale_name = en -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### -[filter:weberror] -use = egg:WebError#error_catcher -debug = false -;error_log = -;show_exceptions_in_wsgi_errors = true -;smtp_server = localhost -;error_email = janitor@example.com -;smtp_username = janitor -;smtp_password = "janitor's password" -;from_address = paste@localhost -;error_subject_prefix = "Pyramid Error" -;smtp_use_tls = -;error_message = +[app:main] +use = egg:tutorial -[filter:tm] -use = egg:repoze.tm2#tm -commit_veto = repoze.tm:default_commit_veto +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en -[pipeline:main] -pipeline = - weberror - tm - tutorial +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] -use = egg:Paste#http +use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -72,6 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/views/setup.cfg b/docs/tutorials/wiki2/src/views/setup.cfg deleted file mode 100644 index 23b2ad983..000000000 --- a/docs/tutorials/wiki2/src/views/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=tutorial -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = tutorial/locale -domain = tutorial -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = tutorial/locale/tutorial.pot -width = 80 - -[init_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale - -[update_catalog] -domain = tutorial -input_file = tutorial/locale/tutorial.pot -output_dir = tutorial/locale -previous = true diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index ae9869d50..def3ce1f6 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -1,35 +1,42 @@ import os -import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'repoze.tm2>=1.0b1', # default_commit_veto 'zope.sqlalchemy', - 'WebError', - 'docutils', + 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='tutorial', version='0.0', description='tutorial', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -37,12 +44,14 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', - install_requires = requires, - entry_points = """\ + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main """, - paster_plugins=['pyramid'], ) - diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index ad89c124e..4dab44823 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -1,25 +1,12 @@ from pyramid.config import Configurator -from sqlalchemy import engine_from_config -from tutorial.models import initialize_sql def main(global_config, **settings): - """ This function returns a WSGI application. + """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - initialize_sql(engine) config = Configurator(settings=settings) - config.add_static_view('static', 'tutorial:static') - config.add_route('view_wiki', '/') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') - config.add_view('tutorial.views.view_wiki', route_name='view_wiki') - config.add_view('tutorial.views.view_page', route_name='view_page', - renderer='tutorial:templates/view.pt') - config.add_view('tutorial.views.add_page', route_name='add_page', - renderer='tutorial:templates/edit.pt') - config.add_view('tutorial.views.edit_page', route_name='edit_page', - renderer='tutorial:templates/edit.pt') + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.scan() return config.make_wsgi_app() - diff --git a/docs/tutorials/wiki2/src/views/tutorial/models.py b/docs/tutorials/wiki2/src/views/tutorial/models.py deleted file mode 100644 index 960c14941..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/models.py +++ /dev/null @@ -1,41 +0,0 @@ -import transaction - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import Text - -from sqlalchemy.exc import IntegrityError -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import scoped_session -from sqlalchemy.orm import sessionmaker - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - def __init__(self, name, data): - self.name = name - self.data = data - -def initialize_sql(engine): - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - try: - transaction.begin() - session = DBSession() - page = Page('FrontPage', 'initial data') - session.add(page) - transaction.commit() - except IntegrityError: - # already created - pass diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +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 + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py new file mode 100644 index 000000000..6bd3315d6 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/routes.py b/docs/tutorials/wiki2/src/views/tutorial/routes.py new file mode 100644 index 000000000..72df58efe --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/routes.py @@ -0,0 +1,6 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s <config_uri> [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico Binary files differdeleted file mode 100644 index 71f837c9e..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/favicon.ico +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png Binary files differdeleted file mode 100644 index 1fbc873da..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/footerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png Binary files differdeleted file mode 100644 index 0596f2020..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/headerbg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/views/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/views/tutorial/static/middlebg.png Binary files differdeleted file mode 100644 index 2369cfb7d..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/middlebg.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css deleted file mode 100644 index fd1914d8d..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/pylons.css +++ /dev/null @@ -1,65 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -:focus{outline:0;} -ins{text-decoration:none;} -del{text-decoration:line-through;} -table{border-collapse:collapse;border-spacing:0;} -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} -#wrap{min-height:100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} -#header{background:#000000;top:0;font-size:14px;} -#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} -.header,.footer{width:750px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#top-small,#bottom{width:100%;} -#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} -#bottom{color:#222;background-color:#ffffff;} -.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} -.top{padding-top:40px;} -.top-small{padding-top:10px;} -#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:350px;float:left;padding-right:25px;} -#right{width:350px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text],input[type=password]{width:205px;} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-16x16.png diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid-small.png +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png Binary files differindex 347e05549..4ab837be9 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png +++ b/docs/tutorials/wiki2/src/views/tutorial/static/pyramid.png diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/theme.css b/docs/tutorials/wiki2/src/views/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/views/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/views/tutorial/static/transparent.gif Binary files differdeleted file mode 100644 index 0341802e5..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/static/transparent.gif +++ /dev/null diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> + <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> +</div> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +<p> +Editing <strong>{{pagename}}</strong> +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +<form action="{{ save_url }}" method="post"> +<div class="form-group"> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> +</div> +<div class="form-group"> + <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> +</div> +</form> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt deleted file mode 100644 index 3f2039cb6..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.pt +++ /dev/null @@ -1,58 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> - </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Editing <b><span tal:replace="page.name">Page Name Goes - Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <form action="${save_url}" method="post"> - <textarea name="body" tal:content="page.data" rows="10" - cols="60"/><br/> - <input type="submit" name="form.submitted" value="Save"/> - </form> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..71785157f --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> + + <!-- Bootstrap core CSS --> + <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> + + <!-- Custom styles for this scaffold --> + <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet"> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + </head> + + <body> + + <div class="starter-template"> + <div class="container"> + <div class="row"> + <div class="col-md-2"> + <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework"> + </div> + <div class="col-md-10"> + <div class="content"> + {% block content %}{% endblock %} + </div> + </div> + </div> + <div class="row"> + <div class="copyright"> + Copyright © Pylons Project + </div> + </div> + </div> + </div> + + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js"></script> + <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js"></script> + </body> +</html> diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +<p>{{ content|safe }}</p> +<p> +<a href="{{ edit_url }}"> + Edit this page +</a> +</p> +<p> + Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>. +</p> +<p>You can return to the +<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. +</p> +{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt deleted file mode 100644 index 423c1d5a1..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt +++ /dev/null @@ -1,61 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" - xmlns:tal="http://xml.zope.org/namespaces/tal"> -<head> - <title>${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> - <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> - <meta name="keywords" content="python web application" /> - <meta name="description" content="pyramid web application" /> - <link rel="shortcut icon" - href="${request.static_url('tutorial:static/favicon.ico')}" /> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/pylons.css')}" - type="text/css" media="screen" charset="utf-8" /> - <!--[if lte IE 6]> - <link rel="stylesheet" - href="${request.static_url('tutorial:static/ie6.css')}" - type="text/css" media="screen" charset="utf-8" /> - <![endif]--> -</head> -<body> - <div id="wrap"> - <div id="top-small"> - <div class="top-small align-center"> - <div> - <img width="220" height="50" alt="pyramid" - src="${request.static_url('tutorial:static/pyramid-small.png')}" /> - </div> - </div> - </div> - <div id="middle"> - <div class="middle align-right"> - <div id="left" class="app-welcome align-left"> - Viewing <b><span tal:replace="page.name">Page Name - Goes Here</span></b><br/> - You can return to the - <a href="${request.application_url}">FrontPage</a>.<br/> - </div> - <div id="right" class="app-welcome align-right"></div> - </div> - </div> - <div id="bottom"> - <div class="bottom"> - <div tal:replace="structure content"> - Page text goes here. - </div> - <p> - <a tal:attributes="href edit_url" href=""> - Edit this page - </a> - </p> - </div> - </div> - </div> - <div id="footer"> - <div class="footer" - >© Copyright 2008-2011, Agendaless Consulting.</div> - </div> -</body> -</html> diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index 0bc343833..99e95efd3 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -1,139 +1,65 @@ import unittest +import transaction from pyramid import testing -def _initTestingDB(): - from tutorial.models import DBSession - from tutorial.models import Base - from sqlalchemy import create_engine - engine = create_engine('sqlite://') - DBSession.configure(bind=engine) - Base.metadata.bind = engine - Base.metadata.create_all(engine) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() - def tearDown(self): - testing.tearDown() + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') + self.session = get_tm_session(session_factory, transaction.manager) -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '<div class="document">\n' - '<p>Hello <a href="http://example.com/add_page/CruelWorld">' - 'CruelWorld</a> ' - '<a href="http://example.com/IDoExist">' - 'IDoExist</a>' - '</p>\n</div>\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - self.config.begin() + from .models.meta import Base - def tearDown(self): - self.session.remove() testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py deleted file mode 100644 index f3d7f4a99..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ /dev/null @@ -1,65 +0,0 @@ -import re - -from docutils.core import publish_parts - -from pyramid.httpexceptions import HTTPFound, HTTPNotFound -from pyramid.url import route_url - -from tutorial.models import DBSession -from tutorial.models import Page - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -def view_wiki(request): - return HTTPFound(location = route_url('view_page', request, - pagename='FrontPage')) - -def view_page(request): - pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') - - def check(match): - word = match.group(1) - exists = session.query(Page).filter_by(name=word).all() - if exists: - view_url = route_url('view_page', request, pagename=word) - return '<a href="%s">%s</a>' % (view_url, word) - else: - add_url = route_url('add_page', request, pagename=word) - return '<a href="%s">%s</a>' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = route_url('edit_page', request, pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url) - -def add_page(request): - name = request.matchdict['pagename'] - if 'form.submitted' in request.params: - session = DBSession() - body = request.params['body'] - page = Page(name, body) - session.add(page) - return HTTPFound(location = route_url('view_page', request, - pagename=name)) - save_url = route_url('add_page', request, pagename=name) - page = Page('', '') - return dict(page=page, save_url=save_url) - -def edit_page(request): - name = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=name).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - session.add(page) - return HTTPFound(location = route_url('view_page', request, - pagename=name)) - return dict( - page=page, - save_url = route_url('edit_page', request, pagename=name), - ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py new file mode 100644 index 000000000..bb6300b75 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -0,0 +1,73 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +from ..models import Page, User + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound('No such page') + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '<a href="%s">%s</a>' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '<a href="%s">%s</a>' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = ( + request.dbsession.query(User).filter_by(name='editor').one()) + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 7a4e65529..e923ff9cb 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -1,74 +1,108 @@ +.. _wiki2_adding_tests: + ============ Adding Tests ============ -We will now add tests for the models and the views and a few functional -tests in the ``tests.py``. Tests ensure that an application works, and -that it continues to work after some changes are made in the future. +We will now add tests for the models and views as well as a few functional +tests in a new ``tests`` subpackage. Tests ensure that an application works, +and that it continues to work when changes are made in the future. -Testing the Models -================== +The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it +is a common practice to put tests into a ``tests`` subpackage, especially as +projects grow in size and complexity. Each module in the test subpackage +should contain tests for its corresponding module in our application. Each +corresponding pair of modules should have the same names, except the test +module should have the prefix ``test_``. -We write a test class for the model class ``Page`` and another test class -for the ``initialize_sql`` function. +Start by deleting ``tests.py``, then create a new directory to contain our new +tests as well as a new empty file ``tests/__init__.py``. -To do so, we'll retain the ``tutorial.tests.ViewTests`` class provided as a -result of the ``pyramid_routesalchemy`` project generator. We'll add two -test classes: one for the ``Page`` model named ``PageModelTests``, one for the -``initialize_sql`` function named ``InitializeSqlTests``. +.. warning:: -Testing the Views -================= + It is very important when refactoring a Python module into a package to be + sure to delete the cache files (``.pyc`` files or ``__pycache__`` folders) + sitting around! Python will prioritize the cache files before traversing + into folders, using the old code, and you will wonder why none of your + changes are working! + + +Test the views +============== + +We'll create a new ``tests/test_views.py`` file, adding a ``BaseTest`` class +used as the base for other test classes. Next we'll add tests for each view +function we previously added to our application. We'll add four test classes: +``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. +These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` +views. -We'll modify our ``tests.py`` file, adding tests for each view function we -added above. As a result, we'll *delete* the ``ViewTests`` test in the file, -and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, -``AddPageTests``, and ``EditPageTests``. These test the ``view_wiki``, -``view_page``, ``add_page``, and ``edit_page`` views respectively. Functional tests ================ -We test the whole application, covering security aspects that are not -tested in the unit tests, like logging in, logging out, checking that -the ``viewer`` user cannot add or edit pages, but the ``editor`` user -can, and so on. +We'll test the whole application, covering security aspects that are not tested +in the unit tests, like logging in, logging out, checking that the ``basic`` +user cannot edit pages that it didn't create but the ``editor`` user can, and +so on. -Viewing the results of all our edits to ``tests.py`` -==================================================== -Once we're done with the ``tests.py`` module, it will look a lot like the -below: +View the results of all our edits to ``tests`` subpackage +========================================================= -.. literalinclude:: src/tests/tutorial/tests.py +Open ``tutorial/tests/test_views.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_views.py :linenos: :language: python -Running the Tests +Open ``tutorial/tests/test_functional.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_functional.py + :linenos: + :language: python + + +.. note:: + + We're utilizing the excellent WebTest_ package to do functional testing of + the application. This is defined in the ``tests_require`` section of our + ``setup.py``. Any other dependencies needed only for testing purposes can be + added there and will be installed automatically when running + ``setup.py test``. + + +Running the tests ================= -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. Assuming our shell's current working directory is the -"tutorial" distribution directory: +We can run these tests similarly to how we did in :ref:`running_tests`: On UNIX: -.. code-block:: text +.. code-block:: bash - $ ../bin/python setup.py test -q + $ $VENV/bin/py.test -q On Windows: -.. code-block:: text +.. code-block:: doscon - c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test -q -The expected result looks something like: +The expected result should look like the following: .. code-block:: text ...................... - ---------------------------------------------------------------------- - Ran 22 tests in 2.700s + 22 passed, 1 pytest-warnings in 5.81 seconds + +.. note:: If you use Python 3 during this tutorial, you will see deprecation + warnings in the output, which we will choose to ignore. In making this + tutorial run on both Python 2 and 3, the authors prioritized simplicity and + focus for the learner over accommodating warnings. In your own app or as + extra credit, you may choose to either drop Python 2 support or hack your + code to work without warnings on both Python 2 and 3. - OK +.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ diff --git a/docs/whatsnew-1.0.rst b/docs/whatsnew-1.0.rst index 8602c5105..0ed6e21fc 100644 --- a/docs/whatsnew-1.0.rst +++ b/docs/whatsnew-1.0.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.0 +What's New in Pyramid 1.0 ========================= This article explains the new features in Pyramid version 1.0 as compared to @@ -62,8 +62,8 @@ fail if you do nothing to your existing :mod:`repoze.bfg` application. However, you won't have to do much to use your existing BFG applications on Pyramid. There's automation which will change most of your import statements and ZCML declarations. See -http://docs.pylonshq.com/pyramid/dev/tutorials/bfg/index.html for upgrade -instructions. +http://docs.pylonsproject.org/projects/pyramid/current/tutorials/bfg/index.html +for upgrade instructions. Pylons 1 users will need to do more work to use Pyramid, as Pyramid shares no "DNA" with Pylons. It is hoped that over time documentation and upgrade code @@ -92,15 +92,15 @@ BFG Conversion Script The ``bfg2pyramid`` conversion script performs a mostly automated conversion of an existing :mod:`repoze.bfg` application to Pyramid. The process is -described in :ref:`converting_a_bfg_app`. +described in "Converting a BFG Application to Pyramid". Scaffold Improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The scaffolds now have much nicer CSS and graphics. -- The ``development.ini``, generated by all scaffolds, is now configured - to use the :term:`WebError` interactive exception debugger by default. +- The ``development.ini``, generated by all scaffolds, is now configured to + use the WebError interactive exception debugger by default. - All scaffolds have been normalized: each now uses the name ``main`` to represent the function that returns a WSGI application, and each now has @@ -110,12 +110,15 @@ Scaffold Improvements (``starter``, ``routesalchemy``, ``alchemy``, ``zodb``) instead of ZCML configuration. -- The ``pyramid_zodb``, ``pyramid_routesalchemy`` and ``pyramid_alchemy`` +- The ``pyramid_zodb``, ``routesalchemy`` and ``pyramid_alchemy`` scaffolds now use a default "commit veto" hook when configuring the ``repoze.tm2`` transaction manager in ``development.ini``. This prevents a transaction from being committed when the response status code is within - the 400 or 500 ranges. See also - http://docs.repoze.org/tm2/#using-a-commit-veto. + the 400 or 500 ranges. + + .. seealso:: + + See also http://docs.repoze.org/tm2/#using-a-commit-veto. Terminology Changes ~~~~~~~~~~~~~~~~~~~ @@ -203,8 +206,8 @@ Mako ~~~~ In addition to Chameleon templating, Pyramid now also provides built-in -support for :term:`Mako` templating. See :ref:`mako_templates` for more -information. +support for :term:`Mako` templating. See +:ref:`available_template_system_bindings` for more information. URL Dispatch ~~~~~~~~~~~~ @@ -249,7 +252,7 @@ ZCML Externalized Pyramid core. Loading ZCML is now a feature of the :term:`pyramid_zcml` package, which can be downloaded from PyPI. Documentation for the package should be available via - http://pylonsproject.org/projects/pyramid_zcml/dev/, which describes how to + http://docs.pylonsproject.org/projects/pyramid_zcml/en/latest/, which describes how to add a configuration statement to your ``main`` block to reobtain this method. You will also need to add an ``install_requires`` dependency upon the ``pyramid_zcml`` distribution to your ``setup.py`` file. diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index ce2f7210a..a5c7f3393 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.1 +What's New in Pyramid 1.1 ========================= This article explains the new features in Pyramid version 1.1 as compared to @@ -7,6 +7,15 @@ incompatibilities between the two versions and deprecations added to Pyramid 1.1, as well as software dependency changes and notable documentation additions. +Terminology Changes +------------------- + +The term "template" used by the Pyramid documentation used to refer to both +"paster templates" and "rendered templates" (templates created by a rendering +engine. i.e. Mako, Chameleon, Jinja, etc.). "Paster templates" will now be +referred to as "scaffolds", whereas the name for "rendered templates" will +remain as "templates." + Major Feature Additions ----------------------- @@ -18,28 +27,47 @@ The major feature additions in Pyramid 1.1 are: - Support for "static" routes. +- Default HTTP exception view. + +- ``http_cache`` view configuration parameter causes Pyramid to set HTTP + caching headers. + +- Features that make it easier to write scripts that work in a :app:`Pyramid` + environment. + ``request.response`` ~~~~~~~~~~~~~~~~~~~~ -- Accessing the ``response`` attribute of a :class:`pyramid.request.Request` - object (e.g. ``request.response`` within a view) now produces a new - :class:`pyramid.response.Response` object. This feature is meant to be - used mainly when a view configured with a renderer needs to set response - attributes: all renderers will use the Response object implied by - ``request.response`` as the response object returned to the router. - - ``request.response`` can also be used by code in a view that does not use a - renderer, however the response object that is produced by - ``request.response`` must be returned when a renderer is not in play (it is - not a "global" response). +- Instances of the :class:`pyramid.request.Request` class now have a + ``response`` attribute. + + The object passed to a view callable as ``request`` is an instance of + :class:`pyramid.request.Request`. ``request.response`` is an instance of + the class :class:`pyramid.response.Response`. View callables that are + configured with a :term:`renderer` will return this response object to the + Pyramid router. Therefore, code in a renderer-using view callable can set + response attributes such as ``request.response.content_type`` (before they + return, e.g. a dictionary to the renderer) and this will influence the HTTP + return value of the view callable. + + ``request.response`` can also be used in view callable code that is not + configured to use a renderer. For example, a view callable might do + ``request.response.body = '123'; return request.response``. However, the + response object that is produced by ``request.response`` must be *returned* + when a renderer is not in play in order to have any effect on the HTTP + response (it is not a "global" response, and modifications to it are not + somehow merged into a separately returned response object). + + The ``request.response`` object is lazily created, so its introduction does + not negatively impact performance. ``paster pviews`` ~~~~~~~~~~~~~~~~~ - A new paster command named ``paster pviews`` was added. This command prints a summary of potentially matching views for a given path. See - documentation the section entitled :ref:`displaying_matching_views` for - more information. + the section entitled :ref:`displaying_matching_views` for more + information. Static Routes ~~~~~~~~~~~~~ @@ -50,13 +78,140 @@ Static Routes be useful for URL generation via ``route_url`` and ``route_path``. See the section entitled :ref:`static_route_narr` for more information. +Default HTTP Exception View +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- A default exception view for the interface + :class:`pyramid.interfaces.IExceptionResponse` is now registered by + default. This means that an instance of any exception class imported from + :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised + from within view code; when raised, this exception view will render the + exception to a response. + + To allow for configuration of this feature, the :term:`Configurator` now + accepts an additional keyword argument named ``exceptionresponse_view``. + By default, this argument is populated with a default exception view + function that will be used when an HTTP exception is raised. When ``None`` + is passed for this value, an exception view for HTTP exceptions will not be + registered. Passing ``None`` returns the behavior of raising an HTTP + exception to that of Pyramid 1.0 (the exception will propagate to + :term:`middleware` and to the WSGI server). + +``http_cache`` +~~~~~~~~~~~~~~ + +A new value ``http_cache`` can be used as a :term:`view configuration` +parameter. + +When you supply an ``http_cache`` value to a view configuration, the +``Expires`` and ``Cache-Control`` headers of a response generated by the +associated view callable are modified. The value for ``http_cache`` may be +one of the following: + +- A nonzero integer. If it's a nonzero integer, it's treated as a number + of seconds. This number of seconds will be used to compute the + ``Expires`` header and the ``Cache-Control: max-age`` parameter of + responses to requests which call this view. For example: + ``http_cache=3600`` instructs the requesting browser to 'cache this + response for an hour, please'. + +- A ``datetime.timedelta`` instance. If it's a ``datetime.timedelta`` + instance, it will be converted into a number of seconds, and that number + of seconds will be used to compute the ``Expires`` header and the + ``Cache-Control: max-age`` parameter of responses to requests which call + this view. For example: ``http_cache=datetime.timedelta(days=1)`` + instructs the requesting browser to 'cache this response for a day, + please'. + +- Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will be + composed such that client browser cache (and any intermediate caches) are + instructed to never cache the response. + +- A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a nonzero + integer or a ``datetime.timedelta`` instance; in either case this value + will be used as the number of seconds to cache the response. The second + value in the tuple must be a dictionary. The values present in the + dictionary will be used as input to the ``Cache-Control`` response + header. For example: ``http_cache=(3600, {'public':True})`` means 'cache + for an hour, and add ``public`` to the Cache-Control header of the + response'. All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to calling + ``response.cache_control.public = True``. + +Providing a non-tuple value as ``http_cache`` is equivalent to calling +``response.cache_expires(value)`` within your view's body. + +Providing a two-tuple value as ``http_cache`` is equivalent to calling +``response.cache_expires(value[0], **value[1])`` within your view's body. + +If you wish to avoid influencing, the ``Expires`` header, and instead wish +to only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` +with the first element of ``None``, e.g.: ``(None, {'public':True})``. + +The environment setting ``PYRAMID_PREVENT_HTTP_CACHE`` and configuration +file value ``prevent_http_cache`` are synonymous and allow you to prevent +HTTP cache headers from being set by Pyramid's ``http_cache`` machinery +globally in a process. see :ref:`influencing_http_caching` and +:ref:`preventing_http_caching`. + +Easier Scripting Writing +~~~~~~~~~~~~~~~~~~~~~~~~ + +A new API function :func:`pyramid.paster.bootstrap` has been added to make +writing scripts that need to work under Pyramid environment easier, e.g.: + +.. code-block:: python + + from pyramid.paster import bootstrap + info = bootstrap('/path/to/my/development.ini') + request = info['request'] + print request.route_url('myroute') + +See :ref:`writing_a_script` for more details. + Minor Feature Additions ----------------------- +- It is now possible to invoke ``paster pshell`` even if the paste ini file + section name pointed to in its argument is not actually a Pyramid WSGI + application. The shell will work in a degraded mode, and will warn the + user. See "The Interactive Shell" in the "Creating a Pyramid Project" + narrative documentation section. + +- The ``paster pshell``, ``paster pviews``, and ``paster proutes`` commands + each now under the hood uses :func:`pyramid.paster.bootstrap`, which makes + it possible to supply an ``.ini`` file without naming the "right" section + in the file that points at the actual Pyramid application. Instead, you + can generally just run ``paster {pshell|proutes|pviews} development.ini`` + and it will do mostly the right thing. + +- It is now possible to add a ``[pshell]`` section to your application's .ini + configuration file, which influences the global names available to a pshell + session. See :ref:`extending_pshell`. + +- The :meth:`pyramid.config.Configurator.scan` method has grown a ``**kw`` + argument. ``kw`` argument represents a set of keyword arguments to pass to + the Venusian ``Scanner`` object created by Pyramid. (See the + :term:`Venusian` documentation for more information about ``Scanner``). + +- New request property: ``json_body``. This property will return the + JSON-decoded variant of the request body. If the request body is not + well-formed JSON, this property will raise an exception. + +- A `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer. See + :ref:`jsonp_renderer` for more details. + - New authentication policy: :class:`pyramid.authentication.SessionAuthenticationPolicy`, which uses a session to store credentials. +- A function named :func:`pyramid.httpexceptions.exception_response` is a + shortcut that can be used to create HTTP exception response objects using + an HTTP integer status code. + - Integers and longs passed as ``elements`` to :func:`pyramid.url.resource_url` or :meth:`pyramid.request.Request.resource_url` e.g. ``resource_url(context, @@ -91,9 +246,171 @@ Minor Feature Additions although typically nonsensical). Allowing the nonsensical configuration made the code more understandable and required fewer tests. +- The :class:`pyramid.request.Request` class now has a ``ResponseClass`` + attribute which points at :class:`pyramid.response.Response`. + +- The :class:`pyramid.response.Response` class now has a ``RequestClass`` + interface which points at :class:`pyramid.request.Request`. + +- It is now possible to return an arbitrary object from a Pyramid view + callable even if a renderer is not used, as long as a suitable adapter to + :class:`pyramid.interfaces.IResponse` is registered for the type of the + returned object by using the new + :meth:`pyramid.config.Configurator.add_response_adapter` API. See the + section in the Hooks chapter of the documentation entitled + :ref:`using_iresponse`. + +- The Pyramid router will now, by default, call the ``__call__`` method of + response objects when returning a WSGI response. This means that, among + other things, the ``conditional_response`` feature response objects + inherited from WebOb will now behave properly. + +- New method named :meth:`pyramid.request.Request.is_response`. This method + should be used instead of the :func:`pyramid.view.is_response` function, + which has been deprecated. + +- :class:`pyramid.exceptions.NotFound` is now just an alias for + :class:`pyramid.httpexceptions.HTTPNotFound`. + +- :class:`pyramid.exceptions.Forbidden` is now just an alias for + :class:`pyramid.httpexceptions.HTTPForbidden`. + +- Added ``mako.preprocessor`` config file parameter; allows for a Mako + preprocessor to be specified as a Python callable or Python dotted name. + See https://github.com/Pylons/pyramid/pull/183 for rationale. + +- New API class: :class:`pyramid.static.static_view`. This supersedes the + (now deprecated) :class:`pyramid.view.static` class. + :class:`pyramid.static.static_view`, by default, serves up documents as the + result of the request's ``path_info``, attribute rather than it's + ``subpath`` attribute (the inverse was true of + :class:`pyramid.view.static`, and still is). + :class:`pyramid.static.static_view` exposes a ``use_subpath`` flag for use + when you want the static view to behave like the older deprecated version. + +- A new api function :func:`pyramid.scripting.prepare` has been added. It is + a lower-level analogue of :func:`pyramid.paster.bootstrap` that accepts a + request and a registry instead of a config file argument, and is used for + the same purpose: + + .. code-block:: python + + from pyramid.scripting import prepare + info = prepare(registry=myregistry) + request = info['request'] + print request.route_url('myroute') + +- A new API function :func:`pyramid.scripting.make_request` has been added. + The resulting request will have a ``registry`` attribute. It is meant to + be used in conjunction with :func:`pyramid.scripting.prepare` and/or + :func:`pyramid.paster.bootstrap` (both of which accept a request as an + argument): + + .. code-block:: python + + from pyramid.scripting import make_request + request = make_request('/') + +- New API attribute :attr:`pyramid.config.global_registries` is an iterable + object that contains references to every Pyramid registry loaded into the + current process via :meth:`pyramid.config.Configurator.make_wsgi_app`. It also + has a ``last`` attribute containing the last registry loaded. This is used + by the scripting machinery, and is available for introspection. + +- Added the :attr:`pyramid.renderers.null_renderer` object as an API. The + null renderer is an object that can be used in advanced integration cases + as input to the view configuration ``renderer=`` argument. When the null + renderer is used as a view renderer argument, Pyramid avoids converting the + view callable result into a Response object. This is useful if you want to + reuse the view configuration and lookup machinery outside the context of + its use by the Pyramid router. (This feature was added for consumption by + the ``pyramid_rpc`` package, which uses view configuration and lookup + outside the context of a router in exactly this way.) + +Backwards Incompatibilities +--------------------------- + +- Pyramid no longer supports Python 2.4. Python 2.5 or better is required to + run Pyramid 1.1+. Pyramid, however, does not work under any version of + Python 3 yet. + +- The Pyramid router now, by default, expects response objects returned from + view callables to implement the :class:`pyramid.interfaces.IResponse` + interface. Unlike the Pyramid 1.0 version of this interface, objects which + implement IResponse now must define a ``__call__`` method that accepts + ``environ`` and ``start_response``, and which returns an ``app_iter`` + iterable, among other things. Previously, it was possible to return any + object which had the three WebOb ``app_iter``, ``headerlist``, and + ``status`` attributes as a response, so this is a backwards + incompatibility. It is possible to get backwards compatibility back by + registering an adapter to IResponse from the type of object you're now + returning from view callables. See the section in the Hooks chapter of the + documentation entitled :ref:`using_iresponse`. + +- The :class:`pyramid.interfaces.IResponse` interface is now much more + extensive. Previously it defined only ``app_iter``, ``status`` and + ``headerlist``; now it is basically intended to directly mirror the + ``webob.Response`` API, which has many methods and attributes. + +- The :mod:`pyramid.httpexceptions` classes named ``HTTPFound``, + ``HTTPMultipleChoices``, ``HTTPMovedPermanently``, ``HTTPSeeOther``, + ``HTTPUseProxy``, and ``HTTPTemporaryRedirect`` now accept ``location`` as + their first positional argument rather than ``detail``. This means that + you can do, e.g. ``return pyramid.httpexceptions.HTTPFound('http://foo')`` + rather than ``return + pyramid.httpexceptions.HTTPFound(location='http//foo')`` (the latter will + of course continue to work). + +- The pyramid Router attempted to set a value into the key + ``environ['repoze.bfg.message']`` when it caught a view-related exception + for backwards compatibility with applications written for :mod:`repoze.bfg` + during error handling. It did this by using code that looked like so:: + + # "why" is an exception object + try: + msg = why[0] + except: + msg = '' + + environ['repoze.bfg.message'] = msg + + Use of the value ``environ['repoze.bfg.message']`` was docs-deprecated in + Pyramid 1.0. Our standing policy is to not remove features after a + deprecation for two full major releases, so this code was originally slated + to be removed in Pyramid 1.2. However, computing the + ``repoze.bfg.message`` value was the source of at least one bug found in + the wild (https://github.com/Pylons/pyramid/issues/199), and there isn't a + foolproof way to both preserve backwards compatibility and to fix the bug. + Therefore, the code which sets the value has been removed in this release. + Code in exception views which relies on this value's presence in the + environment should now use the ``exception`` attribute of the request + (e.g. ``request.exception[0]``) to retrieve the message instead of relying + on ``request.environ['repoze.bfg.message']``. + Deprecations and Behavior Differences ------------------------------------- +.. note:: Under Python 2.7+, it's necessary to pass the Python interpreter + the correct warning flags to see deprecation warnings emitted by Pyramid + when porting your application from an older version of Pyramid. Use the + ``PYTHONWARNINGS`` environment variable with the value ``all`` in the + shell you use to invoke ``paster serve`` to see these warnings, e.g. on + UNIX, ``PYTHONWARNINGS=all $VENV/bin/paster serve development.ini``. + Python 2.5 and 2.6 show deprecation warnings by default, + so this is unnecessary there. + All deprecation warnings are emitted to the console. + +- The :class:`pyramid.view.static` class has been deprecated in favor of the + newer :class:`pyramid.static.static_view` class. A deprecation warning is + raised when it is used. You should replace it with a reference to + :class:`pyramid.static.static_view` with the ``use_subpath=True`` argument. + +- The ``paster pshell``, ``paster proutes``, and ``paster pviews`` commands + now take a single argument in the form ``/path/to/config.ini#sectionname`` + rather than the previous 2-argument spelling ``/path/to/config.ini + sectionname``. ``#sectionname`` may be omitted, in which case ``#main`` is + assumed. + - The default Mako renderer is now configured to escape all HTML in expression tags. This is intended to help prevent XSS attacks caused by rendering unsanitized input from users. To revert this behavior in user's @@ -133,14 +450,17 @@ Deprecations and Behavior Differences spelled:: config.add_route('home', '/') - config.add_view('mypackage.views.myview', route_name='home') + config.add_view('mypackage.views.myview', route_name='home', renderer='some/renderer.pt') This deprecation was done to reduce confusion observed in IRC, as well as - to (eventually) reduce documentation burden (see also - https://github.com/Pylons/pyramid/issues/164). A deprecation warning is - now issued when any view-related parameter is passed to - ``add_route``. + to (eventually) reduce documentation burden. A deprecation warning is + now issued when any view-related parameter is passed to ``add_route``. + + .. seealso:: + + See also `issue #164 on GitHub + <https://github.com/Pylons/pyramid/issues/164>`_. - Passing an ``environ`` dictionary to the ``__call__`` method of a "traverser" (e.g. an object that implements @@ -162,20 +482,22 @@ Deprecations and Behavior Differences expected an environ object in BFG 1.0 and before). In a future version, these methods will be removed entirely. -- A custom request factory is now required to return a response object that - has a ``response`` attribute (or "reified"/lazy property) if they the +- A custom request factory is now required to return a request object that + has a ``response`` attribute (or "reified"/lazy property) if the request is meant to be used in a view that uses a renderer. This ``response`` attribute should be an instance of the class :class:`pyramid.response.Response`. - The JSON and string renderer factories now assign to ``request.response.content_type`` rather than - ``request.response_content_type``. Each renderer factory determines - whether it should change the content type of the response by comparing the - response's content type against the response's default content type; if the - content type is not the default content type (usually ``text/html``), the - renderer changes the content type (to ``application/json`` or - ``text/plain`` for JSON and string renderers respectively). + ``request.response_content_type``. + +- Each built-in renderer factory now determines whether it should change the + content type of the response by comparing the response's content type + against the response's default content type; if the content type is the + default content type (usually ``text/html``), the renderer changes the + content type (to ``application/json`` or ``text/plain`` for JSON and string + renderers respectively). - The :func:`pyramid.wsgi.wsgiapp2` now uses a slightly different method of figuring out how to "fix" ``SCRIPT_NAME`` and ``PATH_INFO`` for the @@ -185,14 +507,99 @@ Deprecations and Behavior Differences - Previously, :class:`pyramid.request.Request` inherited from :class:`webob.request.Request` and implemented ``__getattr__``, - ``__setattr__`` and ``__delattr__`` itself in order to overidde "adhoc + ``__setattr__`` and ``__delattr__`` itself in order to override "adhoc attr" WebOb behavior where attributes of the request are stored in the - environ. Now, :class:`pyramid.request.Request inherits from (the more - recent) :class:`webob.request.BaseRequest`` instead of + environ. Now, :class:`pyramid.request.Request` inherits from (the more + recent) :class:`webob.request.BaseRequest` instead of :class:`webob.request.Request`, which provides the same behavior. :class:`pyramid.request.Request` no longer implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a result. +- Deprecated :func:`pyramid.view.is_response` function in favor of + (newly-added) :meth:`pyramid.request.Request.is_response` method. + Determining if an object is truly a valid response object now requires + access to the registry, which is only easily available as a request + attribute. The :func:`pyramid.view.is_response` function will still work + until it is removed, but now may return an incorrect answer under some + (very uncommon) circumstances. + +- :class:`pyramid.response.Response` is now a *subclass* of + ``webob.response.Response`` (in order to directly implement the + :class:`pyramid.interfaces.IResponse` interface, to speed up response + generation). + +- The "exception response" objects importable from ``pyramid.httpexceptions`` + (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that + actually live in ``webob.exc``. Instead, we've defined our own exception + classes within the module that mirror and emulate the ``webob.exc`` + exception response objects almost entirely. See + :ref:`http_exception_hierarchy` in the Design Defense chapter for more + information. + +- When visiting a URL that represented a static view which resolved to a + subdirectory, the ``index.html`` of that subdirectory would not be served + properly. Instead, a redirect to ``/subdir`` would be issued. This has + been fixed, and now visiting a subdirectory that contains an ``index.html`` + within a static view returns the index.html properly. + + .. seealso:: + + See also `issue #67 on GitHub + <https://github.com/Pylons/pyramid/issues/67>`_. + +- Deprecated the ``pyramid.config.Configurator.set_renderer_globals_factory`` + method and the ``renderer_globals`` Configurator constructor parameter. + Users should convert code using this feature to use a BeforeRender event. See + the section :ref:`beforerender_event` in the Hooks chapter. + +- In Pyramid 1.0, the :class:`pyramid.events.subscriber` directive behaved + contrary to the documentation when passed more than one interface object to + its constructor. For example, when the following listener was registered:: + + @subscriber(IFoo, IBar) + def expects_ifoo_events_and_ibar_events(event): + print event + + The Events chapter docs claimed that the listener would be registered and + listening for both ``IFoo`` and ``IBar`` events. Instead, it registered an + "object event" subscriber which would only be called if an IObjectEvent was + emitted where the object interface was ``IFoo`` and the event interface was + ``IBar``. + + The behavior now matches the documentation. If you were relying on the + buggy behavior of the 1.0 ``subscriber`` directive in order to register an + object event subscriber, you must now pass a sequence to indicate you'd + like to register a subscriber for an object event. e.g.:: + + @subscriber([IFoo, IBar]) + def expects_object_event(object, event): + print object, event + +- In 1.0, if a :class:`pyramid.events.BeforeRender` event subscriber added a + value via the ``__setitem__`` or ``update`` methods of the event object + with a key that already existed in the renderer globals dictionary, a + ``KeyError`` was raised. With the deprecation of the + "add_renderer_globals" feature of the configurator, there was no way to + override an existing value in the renderer globals dictionary that already + existed. Now, the event object will overwrite an older value that is + already in the globals dictionary when its ``__setitem__`` or ``update`` is + called (as well as the new ``setdefault`` method), just like a plain old + dictionary. As a result, for maximum interoperability with other + third-party subscribers, if you write an event subscriber meant to be used + as a BeforeRender subscriber, your subscriber code will now need to (using + ``.get`` or ``__contains__`` of the event object) ensure no value already + exists in the renderer globals dictionary before setting an overriding + value. + +- The :meth:`pyramid.config.Configurator.add_route` method allowed two routes + with the same route to be added without an intermediate call to + :meth:`pyramid.config.Configurator.commit`. If you now receive a + ``ConfigurationError`` at startup time that appears to be ``add_route`` + related, you'll need to either a) ensure that all of your route names are + unique or b) call ``config.commit()`` before adding a second route with the + name of a previously added name or c) use a Configurator that works in + ``autocommit`` mode. + Dependency Changes ------------------ @@ -205,10 +612,8 @@ Dependency Changes Documentation Enhancements -------------------------- -- The term "template" used to refer to both "paster templates" and "rendered - templates" (templates created by a rendering engine. i.e. Mako, Chameleon, - Jinja, etc.). "Paster templates" will now be refered to as "scaffolds", - whereas the name for "rendered templates" will remain as "templates." +- Added a section entitled :ref:`writing_a_script` to the "Command-Line + Pyramid" chapter. - The :ref:`bfg_wiki_tutorial` was updated slightly. @@ -235,3 +640,11 @@ Documentation Enhancements - Added a section to the "URL Dispatch" narrative chapter regarding the new "static" route feature entitled :ref:`static_route_narr`. + +- Added API docs for :func:`pyramid.httpexceptions.exception_response`. + +- Added :ref:`http_exceptions` section to Views narrative chapter including a + description of :func:`pyramid.httpexceptions.exception_response`. + +- Added API docs for + :class:`pyramid.authentication.SessionAuthenticationPolicy`. diff --git a/docs/whatsnew-1.2.rst b/docs/whatsnew-1.2.rst new file mode 100644 index 000000000..9ff933ace --- /dev/null +++ b/docs/whatsnew-1.2.rst @@ -0,0 +1,310 @@ +What's New in Pyramid 1.2 +========================= + +This article explains the new features in :app:`Pyramid` version 1.2 as +compared to its predecessor, :app:`Pyramid` 1.1. It also documents backwards +incompatibilities between the two versions and deprecations added to Pyramid +1.2, as well as software dependency changes and notable documentation +additions. + +Major Feature Additions +----------------------- + +The major feature additions in Pyramid 1.2 follow. + +Debug Toolbar +~~~~~~~~~~~~~ + +The scaffolding packages that come with Pyramid now include a debug toolbar +component which can be used to interactively debug an application. See +:ref:`debug_toolbar` for more information. + +``route_prefix`` Argument to include +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`pyramid.config.Configurator.include` method now accepts a +``route_prefix`` argument. This argument allows you to compose URL dispatch +applications together from disparate packages. See :ref:`route_prefix` for +more information. + +Tweens +~~~~~~ + +A :term:`tween` is used to wrap the Pyramid router's primary request handling +function. This is a feature that can be used by Pyramid framework extensions, +to provide, for example, view timing support and can provide a convenient +place to hang bookkeeping code. Tweens are a little like :term:`WSGI` +:term:`middleware`, but have access to Pyramid functionality such as renderers +and a full-featured request object. + +To support this feature, a new configurator directive exists named +:meth:`pyramid.config.Configurator.add_tween`. This directive adds a +"tween". + +Tweens are further described in :ref:`registering_tweens`. + +A new paster command now exists: ``paster ptweens``. This command prints the +current tween configuration for an application. See the section entitled +:ref:`displaying_tweens` for more info. + +Scaffolding Changes +~~~~~~~~~~~~~~~~~~~ + +- All scaffolds now use the ``pyramid_tm`` package rather than the + ``repoze.tm2`` :term:`middleware` to manage transaction management. + +- The ZODB scaffold now uses the ``pyramid_zodbconn`` package rather than the + ``repoze.zodbconn`` package to provide ZODB integration. + +- All scaffolds now use the ``pyramid_debugtoolbar`` package rather than the + ``WebError`` package to provide interactive debugging features. + +- Projects created via a scaffold no longer depend on the ``WebError`` package + at all; configuration in the ``production.ini`` file which used to require + its ``error_catcher`` :term:`middleware` has been removed. Configuring + error catching / email sending is now the domain of the ``pyramid_exclog`` + package (see http://docs.pylonsproject.org/projects/pyramid_exclog/dev/). + +- All scaffolds now send the ``cache_max_age`` parameter to the + ``add_static_view`` method. + +Minor Feature Additions +----------------------- + +- The ``[pshell]`` section in an ini configuration file now treats a + ``setup`` key as a dotted name that points to a callable that is passed the + bootstrap environment. It can mutate the environment as necessary during a + ``paster pshell`` session. This feature is described in + :ref:`writing_a_script`. + +- A new configuration setting named ``pyramid.includes`` is now available. + It is described in :ref:`including_packages`. + +- Added a :data:`pyramid.security.NO_PERMISSION_REQUIRED` constant for use in + ``permission=`` statements to view configuration. This constant has a + value of the string ``__no_permission_required__``. This string value was + previously referred to in documentation; now the documentation uses the + constant. + +- Added a decorator-based way to configure a response adapter: + :class:`pyramid.response.response_adapter`. This decorator has the same + use as :meth:`pyramid.config.Configurator.add_response_adapter` but it's + declarative. + +- The :class:`pyramid.events.BeforeRender` event now has an attribute named + ``rendering_val``. This can be used to introspect the value returned by a + view in a BeforeRender subscriber. + +- The Pyramid debug logger now uses the standard logging configuration + (usually set up by Paste as part of startup). This means that output from + e.g. ``debug_notfound``, ``debug_authorization``, etc. will go to the + normal logging channels. The logger name of the debug logger will be the + package name of the *caller* of the Configurator's constructor. + +- A new attribute is available on request objects: ``exc_info``. Its value + will be ``None`` until an exception is caught by the Pyramid router, after + which it will be the result of ``sys.exc_info()``. + +- :class:`pyramid.testing.DummyRequest` now implements the + ``add_finished_callback`` and ``add_response_callback`` methods implemented + by :class:`pyramid.request.Request`. + +- New methods of the :class:`pyramid.config.Configurator` class: + :meth:`~pyramid.config.Configurator.set_authentication_policy` and + :meth:`~pyramid.config.Configurator.set_authorization_policy`. These are + meant to be consumed mostly by add-on authors who wish to offer packages + which register security policies. + +- New Configurator method: + :meth:`pyramid.config.Configurator.set_root_factory`, which can set the + root factory after the Configurator has been constructed. + +- Pyramid no longer eagerly commits some default configuration statements at + :term:`Configurator` construction time, which permits values passed in as + constructor arguments (e.g. ``authentication_policy`` and + ``authorization_policy``) to override the same settings obtained via the + :meth:`pyramid.config.Configurator.include` method. + +- Better Mako rendering exceptions; the template line which caused the error + is now shown when a Mako rendering raises an exception. + +- New request methods: :meth:`~pyramid.request.Request.current_route_url`, + :meth:`~pyramid.request.Request.current_route_path`, and + :meth:`~pyramid.request.Request.static_path`. + +- New functions in the :mod:`pyramid.url` module: + :func:`~pyramid.url.current_route_path` and + :func:`~pyramid.url.static_path`. + +- The :meth:`pyramid.request.Request.static_url` API (and its brethren + :meth:`pyramid.request.Request.static_path`, + :func:`pyramid.url.static_url`, and :func:`pyramid.url.static_path`) now + accept an absolute filename as a "path" argument. This will generate a URL + to an asset as long as the filename is in a directory which was previously + registered as a static view. Previously, trying to generate a URL to an + asset using an absolute file path would raise a ValueError. + +- The :class:`~pyramid.authentication.RemoteUserAuthenticationPolicy`, + :class:`~pyramid.authentication.AuthTktAuthenticationPolicy`, and + :class:`~pyramid.authentication.SessionAuthenticationPolicy` constructors + now accept an additional keyword argument named ``debug``. By default, + this keyword argument is ``False``. When it is ``True``, debug information + will be sent to the Pyramid debug logger (usually on stderr) when the + ``authenticated_userid`` or ``effective_principals`` method is called on + any of these policies. The output produced can be useful when trying to + diagnose authentication-related problems. + +- New view predicate: ``match_param``. Example: a view added via + ``config.add_view(aview, match_param='action=edit')`` will be called only + when the ``request.matchdict`` has a value inside it named ``action`` with + a value of ``edit``. + +- Support an ``onerror`` keyword argument to + :meth:`pyramid.config.Configurator.scan`. This argument is passed to + :meth:`venusian.Scanner.scan` to influence error behavior when an exception + is raised during scanning. + +- The ``request_method`` predicate argument to + :meth:`pyramid.config.Configurator.add_view` and + :meth:`pyramid.config.Configurator.add_route` is now permitted to be a + tuple of HTTP method names. Previously it was restricted to being a string + representing a single HTTP method name. + +- Undeprecated ``pyramid.traversal.find_model``, + ``pyramid.traversal.model_path``, ``pyramid.traversal.model_path_tuple``, + and ``pyramid.url.model_url``, which were all deprecated in Pyramid 1.0. + There's just not much cost to keeping them around forever as aliases to + their renamed ``resource_*`` prefixed functions. + +- Undeprecated ``pyramid.view.bfg_view``, which was deprecated in Pyramid + 1.0. This is a low-cost alias to ``pyramid.view.view_config`` which we'll + just keep around forever. + +- Route pattern replacement marker names can now begin with an underscore. + See https://github.com/Pylons/pyramid/issues/276. + +Deprecations +------------ + +- All Pyramid-related :term:`deployment settings` (e.g. ``debug_all``, + ``debug_notfound``) are now meant to be prefixed with the prefix + ``pyramid.``. For example: ``debug_all`` -> ``pyramid.debug_all``. The + old non-prefixed settings will continue to work indefinitely but supplying + them may print a deprecation warning. All scaffolds and tutorials have + been changed to use prefixed settings. + +- The :term:`deployment settings` dictionary now raises a deprecation warning + when you attempt to access its values via ``__getattr__`` instead of via + ``__getitem__``. + +Backwards Incompatibilities +--------------------------- + +- If a string is passed as the ``debug_logger`` parameter to a + :term:`Configurator`, that string is considered to be the name of a global + Python logger rather than a dotted name to an instance of a logger. + +- The :meth:`pyramid.config.Configurator.include` method now accepts only a + single ``callable`` argument. A *sequence* of callables used to be + permitted. If you are passing more than one ``callable`` to + :meth:`pyramid.config.Configurator.include`, it will break. You now must + now instead make a separate call to the method for each callable. + +- It may be necessary to more strictly order configuration route and view + statements when using an "autocommitting" :term:`Configurator`. In the + past, it was possible to add a view which named a route name before adding + a route with that name when you used an autocommitting configurator. For + example: + + .. code-block:: python + + config = Configurator(autocommit=True) + config.add_view('my.pkg.someview', route_name='foo') + config.add_route('foo', '/foo') + + The above will raise an exception when the view attempts to add itself. + Now you must add the route before adding the view: + + .. code-block:: python + + config = Configurator(autocommit=True) + config.add_route('foo', '/foo') + config.add_view('my.pkg.someview', route_name='foo') + + This won't effect "normal" users, only people who have legacy BFG codebases + that used an autommitting configurator and possibly tests that use the + configurator API (the configurator returned by + :func:`pyramid.testing.setUp` is an autocommitting configurator). The + right way to get around this is to use a default non-autocommitting + configurator, which does not have these directive ordering requirements: + + .. code-block:: python + + config = Configurator() + config.add_view('my.pkg.someview', route_name='foo') + config.add_route('foo', '/foo') + + The above will work fine. + +- The :meth:`pyramid.config.Configurator.add_route` directive no longer + returns a route object. This change was required to make route vs. view + configuration processing work properly. + +Behavior Differences +-------------------- + +- An ETag header is no longer set when serving a static file. A + Last-Modified header is set instead. + +- Static file serving no longer supports the ``wsgi.file_wrapper`` extension. + +- Instead of returning a ``403 Forbidden`` error when a static file is served + that cannot be accessed by the Pyramid process' user due to file + permissions, an IOError (or similar) will be raised. + +Documentation Enhancements +-------------------------- + +- Narrative and API documentation which used the ``route_url``, + ``route_path``, ``resource_url``, ``static_url``, and ``current_route_url`` + functions in the :mod:`pyramid.url` package have now been changed to use + eponymous methods of the request instead. + +- Added a section entitled :ref:`route_prefix` to the "URL Dispatch" + narrative documentation chapter. + +- Added a new module to the API docs: :mod:`pyramid.tweens`. + +- Added a :ref:`registering_tweens` section to the "Hooks" narrative chapter. + +- Added a :ref:`displaying_tweens` section to the "Command-Line Pyramid" + narrative chapter. + +- Added documentation for :ref:`explicit_tween_config` and + :ref:`including_packages` to the "Environment Variables and ``.ini`` Files + Settings" chapter. + +- Added a :ref:`logging_chapter` chapter to the narrative docs. + +- All tutorials now use - The ``route_url``, ``route_path``, + ``resource_url``, ``static_url``, and ``current_route_url`` methods of the + :class:`pyramid.request.Request` rather than the function variants imported + from ``pyramid.url``. + +- The ZODB wiki tutorial now uses the ``pyramid_zodbconn`` package rather + than the ``repoze.zodbconn`` package to provide ZODB integration. + +- Added :ref:`what_makes_pyramid_unique` to the Introduction narrative + chapter. + + +Dependency Changes +------------------ + +- Pyramid now relies on PasteScript >= 1.7.4. This version contains a + feature important for allowing flexible logging configuration. + +- Pyramid now requires Venusian 1.0a1 or better to support the ``onerror`` + keyword argument to :meth:`pyramid.config.Configurator.scan`. + +- The ``zope.configuration`` package is no longer a dependency. diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst new file mode 100644 index 000000000..8de69c450 --- /dev/null +++ b/docs/whatsnew-1.3.rst @@ -0,0 +1,575 @@ +What's New in Pyramid 1.3 +========================= + +This article explains the new features in :app:`Pyramid` version 1.3 as +compared to its predecessor, :app:`Pyramid` 1.2. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.3, as well as software dependency changes and notable +documentation additions. + +Major Feature Additions +----------------------- + +The major feature additions in Pyramid 1.3 follow. + +Python 3 Compatibility +~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: python-3.png + +Pyramid continues to run on Python 2, but Pyramid is now also Python 3 +compatible. To use Pyramid under Python 3, Python 3.3 or better is required. + +Many Pyramid add-ons are already Python 3 compatible. For example, +``pyramid_debugtoolbar``, ``pyramid_jinja2``, ``pyramid_exclog``, +``pyramid_tm``, ``pyramid_mailer``, and ``pyramid_handlers`` are all Python +3-ready. But other add-ons are known to work only under Python 2. Also, +some scaffolding dependencies (particularly ZODB) do not yet work under +Python 3. + +Please be patient as we gain full ecosystem support for Python 3. You can +see more details about ongoing porting efforts at +https://github.com/Pylons/pyramid/wiki/Python-3-Porting . + +Python 3 compatibility required dropping some package dependencies and +support for older Python versions and platforms. See the "Backwards +Incompatibilities" section below for more information. + +The ``paster`` Command Has Been Replaced +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We've replaced the ``paster`` command with Pyramid-specific analogues. Why? +The libraries that supported the ``paster`` command named ``Paste`` and +``PasteScript`` do not run under Python 3, and we were unwilling to port and +maintain them ourselves. As a result, we've had to make some changes. + +Previously (in Pyramid 1.0, 1.1 and 1.2), you created a Pyramid application +using ``paster create``, like so:: + + $ $VENV/bin/paster create -t pyramid_starter foo + +In 1.3, you're now instead required to create an application using +``pcreate`` like so:: + + $ $VENV/bin/pcreate -s starter foo + +``pcreate`` is required to be used for internal Pyramid scaffolding; +externally distributed scaffolding may allow for both ``pcreate`` and/or +``paster create``. + +In previous Pyramid versions, you ran a Pyramid application like so:: + + $ $VENV/bin/paster serve development.ini + +Instead, you now must use the ``pserve`` command in 1.3:: + + $ $VENV/bin/pserve development.ini + +The ``ini`` configuration file format supported by Pyramid has not changed. +As a result, Python 2-only users can install PasteScript manually and use +``paster serve`` instead if they like. However, using ``pserve`` will work +under both Python 2 and Python 3. + +Analogues of ``paster pshell``, ``paster pviews``, ``paster request`` and +``paster ptweens`` also exist under the respective console script names +``pshell``, ``pviews``, ``prequest`` and ``ptweens``. + +``paste.httpserver`` replaced by ``waitress`` in Scaffolds +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Because the ``paste.httpserver`` server we used previously in scaffolds is +not Python 3 compatible, we've made the default WSGI server used by Pyramid +scaffolding the :term:`waitress` server. The waitress server is both Python +2 and Python 3 compatible. + +Once you create a project from a scaffold, its ``development.ini`` and +``production.ini`` will have the following line:: + + use = egg:waitress#main + +Instead of this (which was the default in older versions):: + + use = egg:Paste#http + +.. note:: + + ``paste.httpserver`` "helped" by converting header values that were Unicode + into strings, which was a feature that subverted the :term:`WSGI` + specification. The ``waitress`` server, on the other hand implements the + WSGI spec more fully. This specifically may affect you if you are modifying + headers on your responses. The following error might be an indicator of + this problem: **AssertionError: Header values must be strings, please check + the type of the header being returned.** A common case would be returning + Unicode headers instead of string headers. + +Compatibility Helper Library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new :mod:`pyramid.compat` module was added which provides Python 2/3 +straddling support for Pyramid add-ons and development environments. + +Introspection +~~~~~~~~~~~~~ + +A configuration introspection system was added; see +:ref:`using_introspection` and :ref:`introspection` for more information on +using the introspection system as a developer. + +The latest release of the pyramid debug toolbar (0.9.7+) provides an +"Introspection" panel that exposes introspection information to a Pyramid +application developer. + +New APIs were added to support introspection +:attr:`pyramid.registry.Introspectable`, +:attr:`pyramid.config.Configurator.introspector`, +:attr:`pyramid.config.Configurator.introspectable`, +:attr:`pyramid.registry.Registry.introspector`. + +``@view_defaults`` Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use a class as a view, you can use the new +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. + +For instance, if you've got a class that has methods that represent "REST +actions", all which are mapped to the same route, but different request +methods, instead of this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') + + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(request_method='GET') + def get(self): + return Response('get') + + @view_config(request_method='POST') + def post(self): + return Response('post') + + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +This also works for imperative view configurations that involve a class. + +See :ref:`view_defaults` for more information. + +Extending a Request without Subclassing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is now possible to extend a :class:`pyramid.request.Request` object +with property descriptors without having to create a custom request factory. +The new method :meth:`pyramid.config.Configurator.set_request_property` +provides an entry point for addons to register properties which will be +added to each request. New properties may be reified, effectively caching +the return value for the lifetime of the instance. Common use-cases for this +would be to get a database connection for the request or identify the current +user. The new method :meth:`pyramid.request.Request.set_property` has been +added, as well, but the configurator method should be preferred as it +provides conflict detection and consistency in the lifetime of the +properties. + +Not Found and Forbidden View Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not Found helpers: + +- New API: :meth:`pyramid.config.Configurator.add_notfound_view`. This is a + wrapper for :meth:`pyramid.config.Configurator.add_view` which provides + support for an "append_slash" feature as well as doing the right thing when + it comes to permissions (a Not Found View should always be public). It + should be preferred over calling ``add_view`` directly with + ``context=HTTPNotFound`` as was previously recommended. + +- New API: :class:`pyramid.view.notfound_view_config`. This is a decorator + constructor like :class:`pyramid.view.view_config` that calls + :meth:`pyramid.config.Configurator.add_notfound_view` when scanned. It + should be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPNotFound`` as was previously recommended. + +Forbidden helpers: + +- New API: :meth:`pyramid.config.Configurator.add_forbidden_view`. This is a + wrapper for :meth:`pyramid.config.Configurator.add_view` which does the + right thing about permissions. It should be preferred over calling + ``add_view`` directly with ``context=HTTPForbidden`` as was previously + recommended. + +- New API: :class:`pyramid.view.forbidden_view_config`. This is a decorator + constructor like :class:`pyramid.view.view_config` that calls + :meth:`pyramid.config.Configurator.add_forbidden_view` when scanned. It + should be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + +Minor Feature Additions +----------------------- + +- New APIs: :class:`pyramid.path.AssetResolver` and + :class:`pyramid.path.DottedNameResolver`. The former can be used to + resolve an :term:`asset specification` to an API that can be used to read + the asset's data, the latter can be used to resolve a :term:`dotted Python + name` to a module or a package. + +- A ``mako.directories`` setting is no longer required to use Mako templates + Rationale: Mako template renderers can be specified using an absolute asset + spec. An entire application can be written with such asset specs, + requiring no ordered lookup path. + +- ``bpython`` interpreter compatibility in ``pshell``. See + :ref:`ipython_or_bpython` for more information. + +- Added :func:`pyramid.paster.get_appsettings` API function. This function + returns the settings defined within an ``[app:...]`` section in a + PasteDeploy ``ini`` file. + +- Added :func:`pyramid.paster.setup_logging` API function. This function + sets up Python logging according to the logging configuration in a + PasteDeploy ``ini`` file. + +- Configuration conflict reporting is reported in a more understandable way + ("Line 11 in file..." vs. a repr of a tuple of similar info). + +- We allow extra keyword arguments to be passed to the + :meth:`pyramid.config.Configurator.action` method. + +- Responses generated by Pyramid's :class:`pyramid.static.static_view` now use + a ``wsgi.file_wrapper`` (see + http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling) + when one is provided by the web server. + +- The :meth:`pyramid.config.Configurator.scan` method can be passed an + ``ignore`` argument, which can be a string, a callable, or a list + consisting of strings and/or callables. This feature allows submodules, + subpackages, and global objects from being scanned. See + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + more information about how to use the ``ignore`` argument to ``scan``. + +- Add :meth:`pyramid.config.Configurator.add_traverser` API method. See + :ref:`changing_the_traverser` for more information. This is not a new + feature, it just provides an API for adding a traverser without needing to + use the ZCA API. + +- Add :meth:`pyramid.config.Configurator.add_resource_url_adapter` API + method. See :ref:`changing_resource_url` for more information. This is + not a new feature, it just provides an API for adding a resource url + adapter without needing to use the ZCA API. + +- Better error messages when a view callable returns a value that cannot be + converted to a response (for example, when a view callable returns a + dictionary without a renderer defined, or doesn't return any value at all). + The error message now contains information about the view callable itself + as well as the result of calling it. + +- Better error message when a .pyc-only module is ``config.include`` -ed. + This is not permitted due to error reporting requirements, and a better + error message is shown when it is attempted. Previously it would fail with + something like "AttributeError: 'NoneType' object has no attribute + 'rfind'". + +- The system value ``req`` is now supplied to renderers as an alias for + ``request``. This means that you can now, for example, in a template, do + ``req.route_url(...)`` instead of ``request.route_url(...)``. This is + purely a change to reduce the amount of typing required to use request + methods and attributes from within templates. The value ``request`` is + still available too, this is just an alternative. + +- A new interface was added: :class:`pyramid.interfaces.IResourceURL`. An + adapter implementing its interface can be used to override resource URL + generation when :meth:`pyramid.request.Request.resource_url` is called. + This interface replaces the now-deprecated + ``pyramid.interfaces.IContextURL`` interface. + +- The dictionary passed to a resource's ``__resource_url__`` method (see + :ref:`overriding_resource_url_generation`) now contains an ``app_url`` key, + representing the application URL generated during + :meth:`pyramid.request.Request.resource_url`. It represents a potentially + customized URL prefix, containing potentially custom scheme, host and port + information passed by the user to ``request.resource_url``. It should be + used instead of ``request.application_url`` where necessary. + +- The :meth:`pyramid.request.Request.resource_url` API now accepts these + arguments: ``app_url``, ``scheme``, ``host``, and ``port``. The app_url + argument can be used to replace the URL prefix wholesale during url + generation. The ``scheme``, ``host``, and ``port`` arguments can be used + to replace the respective default values of ``request.application_url`` + partially. + +- A new API named :meth:`pyramid.request.Request.resource_path` now exists. + It works like :meth:`pyramid.request.Request.resource_url` but produces a + relative URL rather than an absolute one. + +- The :meth:`pyramid.request.Request.route_url` API now accepts these + arguments: ``_app_url``, ``_scheme``, ``_host``, and ``_port``. The + ``_app_url`` argument can be used to replace the URL prefix wholesale + during url generation. The ``_scheme``, ``_host``, and ``_port`` arguments + can be used to replace the respective default values of + ``request.application_url`` partially. + +- New APIs: :class:`pyramid.response.FileResponse` and + :class:`pyramid.response.FileIter`, for usage in views that must serve + files "manually". + +Backwards Incompatibilities +--------------------------- + +- Pyramid no longer runs on Python 2.5. This includes the most recent + release of Jython and the Python 2.5 version of Google App Engine. + + The reason? We could not easily "straddle" Python 2 and 3 versions and + support Python 2 versions older than Python 2.6. You will need Python 2.6 + or better to run this version of Pyramid. If you need to use Python 2.5, + you should use the most recent 1.2.X release of Pyramid. + +- The names of available scaffolds have changed and the flags supported by + ``pcreate`` are different than those that were supported by ``paster + create``. For example, ``pyramid_alchemy`` is now just ``alchemy``. + +- The ``paster`` command is no longer the documented way to create projects, + start the server, or run debugging commands. To create projects from + scaffolds, ``paster create`` is replaced by the ``pcreate`` console script. + To serve up a project, ``paster serve`` is replaced by the ``pserve`` + console script. New console scripts named ``pshell``, ``pviews``, + ``proutes``, and ``ptweens`` do what their ``paster <commandname>`` + equivalents used to do. All relevant narrative documentation has been + updated. Rationale: the Paste and PasteScript packages do not run under + Python 3. + +- The default WSGI server run as the result of ``pserve`` from newly rendered + scaffolding is now the ``waitress`` WSGI server instead of the + ``paste.httpserver`` server. Rationale: the Paste and PasteScript packages + do not run under Python 3. + +- The ``pshell`` command (see "paster pshell") no longer accepts a + ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` + or ``--python-shell`` argument, which can be any of the values ``python``, + ``ipython`` or ``bpython``. + +- Removed the ``pyramid.renderers.renderer_from_name`` function. It has been + deprecated since Pyramid 1.0, and was never an API. + +- To use ZCML with versions of Pyramid >= 1.3, you will need ``pyramid_zcml`` + version >= 0.8 and ``zope.configuration`` version >= 3.8.0. The + ``pyramid_zcml`` package version 0.8 is backwards compatible all the way to + Pyramid 1.0, so you won't be warned if you have older versions installed + and upgrade Pyramid itself "in-place"; it may simply break instead + (particularly if you use ZCML's ``includeOverrides`` directive). + +- String values passed to :meth:`pyramid.request.Request.route_url` or + :meth:`pyramid.request.Request.route_path` that are meant to replace + "remainder" matches will now be URL-quoted except for embedded slashes. For + example:: + + config.add_route('remain', '/foo*remainder') + request.route_path('remain', remainder='abc / def') + # -> '/foo/abc%20/%20def' + + Previously string values passed as remainder replacements were tacked on + untouched, without any URL-quoting. But this doesn't really work logically + if the value passed is Unicode (raw unicode cannot be placed in a URL or in + a path) and it is inconsistent with the rest of the URL generation + machinery if the value is a string (it won't be quoted unless by the + caller). + + Some folks will have been relying on the older behavior to tack on query + string elements and anchor portions of the URL; sorry, you'll need to + change your code to use the ``_query`` and/or ``_anchor`` arguments to + ``route_path`` or ``route_url`` to do this now. + +- If you pass a bytestring that contains non-ASCII characters to + :meth:`pyramid.config.Configurator.add_route` as a pattern, it will now + fail at startup time. Use Unicode instead. + +- The ``path_info`` route and view predicates now match against + ``request.upath_info`` (Unicode) rather than ``request.path_info`` + (indeterminate value based on Python 3 vs. Python 2). This has to be done + to normalize matching on Python 2 and Python 3. + +- The ``match_param`` view predicate no longer accepts a dict. This will have + no negative affect because the implementation was broken for dict-based + arguments. + +- The ``pyramid.interfaces.IContextURL`` interface has been deprecated. + People have been instructed to use this to register a resource url adapter + in the "Hooks" chapter to use to influence + :meth:`pyramid.request.Request.resource_url` URL generation for resources + found via custom traversers since Pyramid 1.0. + + The interface still exists and registering an adapter using it as + documented in older versions still works, but this interface will be + removed from the software after a few major Pyramid releases. You should + replace it with an equivalent :class:`pyramid.interfaces.IResourceURL` + adapter, registered using the new + :meth:`pyramid.config.Configurator.add_resource_url_adapter` API. A + deprecation warning is now emitted when a + ``pyramid.interfaces.IContextURL`` adapter is found when + :meth:`pyramid.request.Request.resource_url` is called. + +- Remove ``pyramid.config.Configurator.with_context`` class method. It was + never an API, it is only used by ``pyramid_zcml`` and its functionality has + been moved to that package's latest release. This means that you'll need + to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of + Pyramid. + +- The older deprecated ``set_notfound_view`` Configurator method is now an + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view`` Configurator method. This has the following impact: + the ``context`` sent to views with a ``(context, request)`` call signature + registered via the ``set_notfound_view`` or ``set_forbidden_view`` will now + be an exception object instead of the actual resource context found. Use + ``request.context`` to get the actual resource context. It's also + recommended to disuse ``set_notfound_view`` in favor of + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. + +Deprecations +------------ + +- The API documentation for ``pyramid.view.append_slash_notfound_view`` and + ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names + still exist and are still importable, but they are no longer APIs. Use + ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or + ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same + behavior. + +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. + +- All references to the ``tmpl_context`` request variable were removed from + the docs. Its existence in Pyramid is confusing for people who were never + Pylons users. It was added as a porting convenience for Pylons users in + Pyramid 1.0, but it never caught on because the Pyramid rendering system is + a lot different than Pylons' was, and alternate ways exist to do what it + was designed to offer in Pylons. It will continue to exist "forever" but + it will not be recommended or mentioned in the docs. + +- Remove references to do-nothing ``pyramid.debug_templates`` setting in all + Pyramid-provided .ini files. This setting previously told Chameleon to render + better exceptions; now Chameleon always renders nice exceptions regardless of + the value of this setting. + +Known Issues +------------ + +- As of this writing (the release of Pyramid 1.3b2), if you attempt to + install a Pyramid project that used the ``alchemy`` scaffold via ``setup.py + develop`` on Python 3.2, it will quit with an installation error while + trying to install ``Pygments``. If this happens, please just rerun the + ``setup.py develop`` command again, and it will complete successfully. + This is due to a minor bug in SQLAlchemy 0.7.5 under Python 3, and has been + fixed in a later SQLAlchemy release. Keep an eye on + http://www.sqlalchemy.org/trac/ticket/2421 + +Documentation Enhancements +-------------------------- + +- The :ref:`bfg_sql_wiki_tutorial` has been updated. It now uses + ``@view_config`` decorators and an explicit database population script. + +- Minor updates to the :ref:`bfg_wiki_tutorial`. + +- A narrative documentation chapter named :ref:`extconfig_narr` was added; it + describes how to add a custom :term:`configuration directive`, and how use + the :meth:`pyramid.config.Configurator.action` method within custom + directives. It also describes how to add :term:`introspectable` objects. + +- A narrative documentation chapter named :ref:`using_introspection` was + added. It describes how to query the introspection system. + +- Added an API docs chapter for :mod:`pyramid.scaffolds`. + +- Added a narrative docs chapter named :ref:`scaffolding_chapter`. + +- Added a description of the ``prequest`` command-line script at + :ref:`invoking_a_request`. + +- Added a section to the "Command-Line Pyramid" chapter named + :ref:`making_a_console_script`. + +- Removed the "Running Pyramid on Google App Engine" tutorial from the main + docs. It survives on in the Pyramid Community Cookbook as + :ref:`Pyramid on Google's App Engine (using appengine-monkey) + <cookbook:appengine_tutorial>`. Rationale: it provides the correct info for + the Python 2.5 version of GAE only, and this version of Pyramid does not + support Python 2.5. + +- Updated the :ref:`changing_the_forbidden_view` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_forbidden_view`` or ``forbidden_view_config``. + +- Updated the :ref:`changing_the_notfound_view` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_notfound_view`` or ``notfound_view_config``. + +- Updated the :ref:`redirecting_to_slash_appended_routes` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_notfound_view`` or ``notfound_view_config`` + +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + +Dependency Changes +------------------ + +- Pyramid no longer depends on the ``zope.component`` package, except as a + testing dependency. + +- Pyramid now depends on the following package versions: + zope.interface>=3.8.0, WebOb>=1.2dev, repoze.lru>=0.4, + zope.deprecation>=3.5.0, translationstring>=0.4 for Python 3 compatibility + purposes. It also, as a testing dependency, depends on WebTest>=1.3.1 for + the same reason. + +- Pyramid no longer depends on the ``Paste`` or ``PasteScript`` packages. + These packages are not Python 3 compatible. + +- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. + +Scaffolding Changes +------------------- + +- Rendered scaffolds have now been changed to be more relocatable (fewer + mentions of the package name within files in the package). + +- The ``routesalchemy`` scaffold has been renamed ``alchemy``, replacing the + older (traversal-based) ``alchemy`` scaffold (which has been retired). + +- The ``alchemy`` and ``starter`` scaffolds are Python 3 compatible. + +- The ``starter`` scaffold now uses URL dispatch by default. diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst new file mode 100644 index 000000000..fce889854 --- /dev/null +++ b/docs/whatsnew-1.4.rst @@ -0,0 +1,367 @@ +What's New in Pyramid 1.4 +========================= + +This article explains the new features in :app:`Pyramid` version 1.4 as +compared to its predecessor, :app:`Pyramid` 1.3. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.4, as well as software dependency changes and notable +documentation additions. + +Major Feature Additions +----------------------- + +The major feature additions in Pyramid 1.4 follow. + +Third-Party Predicates +~~~~~~~~~~~~~~~~~~~~~~~ + +- Third-party custom view, route, and subscriber predicates can now be added + for use by view authors via + :meth:`pyramid.config.Configurator.add_view_predicate`, + :meth:`pyramid.config.Configurator.add_route_predicate` and + :meth:`pyramid.config.Configurator.add_subscriber_predicate`. So, for + example, doing this:: + + config.add_view_predicate('abc', my.package.ABCPredicate) + + Might allow a view author to do this in an application that configured that + predicate:: + + @view_config(abc=1) + + Similar features exist for :meth:`pyramid.config.Configurator.add_route`, + and :meth:`pyramid.config.Configurator.add_subscriber`. See + :ref:`registering_thirdparty_predicates` for more information. + +Easy Custom JSON Serialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Views can now return custom objects which will be serialized to JSON by a + JSON renderer by defining a ``__json__`` method on the object's class. This + method should return values natively serializable by ``json.dumps`` (such + as ints, lists, dictionaries, strings, and so forth). See + :ref:`json_serializing_custom_objects` for more information. The JSON + renderer now also allows for the definition of custom type adapters to + convert unknown objects to JSON serializations, in case you can't add a + ``__json__`` method to returned objects. + +Partial Mako and Chameleon Template Renderings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- The Mako renderer now supports using a def name in an asset spec. When the + def name is present in the asset spec, the system will render the template + named def within the template instead of rendering the entire template. An + example asset spec which names a def is + ``package:path/to/template#defname.mako``. This will render the def named + ``defname`` inside the ``template.mako`` template instead of rendering the + entire template. The old way of returning a tuple in the form + ``('defname', {})`` from the view is supported for backward compatibility. + +- The Chameleon ZPT renderer now supports using a macro name in an asset + spec. When the macro name is present in the asset spec, the system will + render the macro listed as a ``define-macro`` and return the result instead + of rendering the entire template. An example asset spec: + ``package:path/to/template#macroname.pt``. This will render the macro + defined as ``macroname`` within the ``template.pt`` template instead of the + entire template. + +Subrequest Support +~~~~~~~~~~~~~~~~~~ + +- Developers may invoke a subrequest by using the + :meth:`pyramid.request.Request.invoke_subrequest` API. This allows a + developer to obtain a response from one view callable by issuing a subrequest + from within a different view callable. See :ref:`subrequest_chapter` for + more information. + +Minor Feature Additions +----------------------- + +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` has been updated + to support newer hashing algorithms such as ``sha512``. Existing applications + should consider updating if possible for improved security over the default + md5 hashing. + +- :meth:`pyramid.config.Configurator.add_directive` now accepts arbitrary + callables like partials or objects implementing ``__call__`` which don't + have ``__name__`` and ``__doc__`` attributes. See + https://github.com/Pylons/pyramid/issues/621 and + https://github.com/Pylons/pyramid/pull/647. + +- As of this release, the ``request_method`` view/route predicate, when used, + will also imply that ``HEAD`` is implied when you use ``GET``. For + example, using ``@view_config(request_method='GET')`` is equivalent to + using ``@view_config(request_method=('GET', 'HEAD'))``. Using + ``@view_config(request_method=('GET', 'POST')`` is equivalent to using + ``@view_config(request_method=('GET', 'HEAD', 'POST')``. This is because + HEAD is a variant of GET that omits the body, and WebOb has special support + to return an empty body when a HEAD is used. + +- :meth:`pyramid.config.Configurator.add_request_method` has been introduced + to support extending request objects with arbitrary callables. This method + expands on the now documentation-deprecated + :meth:`pyramid.config.Configurator.set_request_property` by supporting + methods as well as properties. This method also causes less code to be + executed at request construction time than + :meth:`~pyramid.config.Configurator.set_request_property`. + +- The static view machinery now raises rather than returns + :class:`pyramid.httpexceptions.HTTPNotFound` and + :class:`pyramid.httpexceptions.HTTPMovedPermanently` exceptions, so these can + be caught by the Not Found View (and other exception views). + +- When there is a predicate mismatch exception (seen when no view matches for + a given request due to predicates not working), the exception now contains + a textual description of the predicate which didn't match. + +- An :meth:`pyramid.config.Configurator.add_permission` directive method was + added to the Configurator. This directive registers a free-standing + permission introspectable into the Pyramid introspection system. + Frameworks built atop Pyramid can thus use the ``permissions`` + introspectable category data to build a comprehensive list of permissions + supported by a running system. Before this method was added, permissions + were already registered in this introspectable category as a side effect of + naming them in an :meth:`pyramid.config.Configurator.add_view` call, this + method just makes it possible to arrange for a permission to be put into + the ``permissions`` introspectable category without naming it along with an + associated view. Here's an example of usage of ``add_permission``:: + + config = Configurator() + config.add_permission('view') + +- The :func:`pyramid.session.UnencryptedCookieSessionFactoryConfig` function + now accepts ``signed_serialize`` and ``signed_deserialize`` hooks which may + be used to influence how the sessions are marshalled (by default this is + done with HMAC+pickle). + +- :class:`pyramid.testing.DummyRequest` now supports methods supplied by the + ``pyramid.util.InstancePropertyMixin`` class such as ``set_property``. + +- Request properties and methods added via + :meth:`pyramid.config.Configurator.add_request_method` or + :meth:`pyramid.config.Configurator.set_request_property` are now available to + tweens. + +- Request properties and methods added via + :meth:`pyramid.config.Configurator.add_request_method` or + :meth:`pyramid.config.Configurator.set_request_property` are now available + in the request object returned from :func:`pyramid.paster.bootstrap`. + +- ``request.context`` of environment request during + :func:`pyramid.paster.bootstrap` is now the root object if a context isn't + already set on a provided request. + +- :class:`pyramid.decorator.reify` is now an API, and was added to + the API documentation. + +- Added the :func:`pyramid.testing.testConfig` context manager, which can be + used to generate a configurator in a test, e.g. ``with + testing.testConfig(...):``. + +- A new :func:`pyramid.session.check_csrf_token` convenience API function was + added. + +- A ``check_csrf`` view predicate was added. For example, you can now do + ``config.add_view(someview, check_csrf=True)``. When the predicate is + checked, if the ``csrf_token`` value in ``request.params`` matches the csrf + token in the request's session, the view will be permitted to execute. + Otherwise, it will not be permitted to execute. + +- Add ``Base.metadata.bind = engine`` to ``alchemy`` scaffold, so that tables + defined imperatively will work. + +- Comments with references to documentation sections placed in scaffold + ``.ini`` files. + +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + +- Added an HTTP Basic authentication policy + at :class:`pyramid.authentication.BasicAuthAuthenticationPolicy`. + +- The :meth:`pyramid.config.Configurator.testing_securitypolicy` method now + returns the policy object it creates. + +- The DummySecurityPolicy created by + :meth:`pyramid.config.Configurator.testing_securitypolicy` now sets a + ``forgotten`` value on the policy (the value ``True``) when its ``forget`` + method is called. + +- The DummySecurityPolicy created by + :meth:`pyramid.config.Configurator.testing_securitypolicy` now sets a + ``remembered`` value on the policy, which is the value of the ``principal`` + argument it's called with when its ``remember`` method is called. + +- New ``physical_path`` view predicate. If specified, this value should be a + string or a tuple representing the physical traversal path of the context + found via traversal for this predicate to match as true. For example: + ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('', + 'a', 'b', 'c')``. It's useful when you want to always potentially show a + view when some object is traversed to, but you can't be sure about what kind + of object it will be, so you can't use the ``context`` predicate. + +- Added an ``effective_principals`` route and view predicate. + +- Do not allow the userid returned from the + :func:`pyramid.security.authenticated_userid` or the userid that is one of the + list of principals returned by :func:`pyramid.security.effective_principals` + to be either of the strings ``system.Everyone`` or ``system.Authenticated`` + when any of the built-in authorization policies that live in + :mod:`pyramid.authentication` are in use. These two strings are reserved for + internal usage by Pyramid and they will no longer be accepted as valid + userids. + +- Allow a ``_depth`` argument to :class:`pyramid.view.view_config`, which will + permit limited composition reuse of the decorator by other software that + wants to provide custom decorators that are much like view_config. + +- Allow an iterable of decorators to be passed to + :meth:`pyramid.config.Configurator.add_view`. This allows views to be wrapped + by more than one decorator without requiring combining the decorators + yourself. + +- :func:`pyramid.security.view_execution_permitted` used to return `True` if no + view could be found. It now raises a :exc:`TypeError` exception in that case, + as it doesn't make sense to assert that a nonexistent view is + execution-permitted. See https://github.com/Pylons/pyramid/issues/299. + +- Small microspeed enhancement which anticipates that a + :class:`pyramid.response.Response` object is likely to be returned from a + view. Some code is shortcut if the class of the object returned by a view is + this class. A similar microoptimization was done to + :func:`pyramid.request.Request.is_response`. + +- Make it possible to use variable arguments on all ``p*`` commands + (``pserve``, ``pshell``, ``pviews``, etc) in the form ``a=1 b=2`` so you can + fill in values in parameterized ``.ini`` file, e.g. ``pshell + etc/development.ini http_port=8080``. + +- In order to allow people to ignore unused arguments to subscriber callables + and to normalize the relationship between event subscribers and subscriber + predicates, we now allow both subscribers and subscriber predicates to accept + only a single ``event`` argument even if they've been subscribed for + notifications that involve multiple interfaces. + +Backwards Incompatibilities +--------------------------- + +- The Pyramid router no longer adds the values ``bfg.routes.route`` or + ``bfg.routes.matchdict`` to the request's WSGI environment dictionary. + These values were docs-deprecated in ``repoze.bfg`` 1.0 (effectively seven + minor releases ago). If your code depended on these values, use + ``request.matched_route`` and ``request.matchdict`` instead. + +- It is no longer possible to pass an environ dictionary directly to + ``pyramid.traversal.ResourceTreeTraverser.__call__`` (aka + ``ModelGraphTraverser.__call__``). Instead, you must pass a request + object. Passing an environment instead of a request has generated a + deprecation warning since Pyramid 1.1. + +- Pyramid will no longer work properly if you use the + ``webob.request.LegacyRequest`` as a request factory. Instances of the + LegacyRequest class have a ``request.path_info`` which return a string. + This Pyramid release assumes that ``request.path_info`` will + unconditionally be Unicode. + +- The functions from ``pyramid.chameleon_zpt`` and ``pyramid.chameleon_text`` + named ``get_renderer``, ``get_template``, ``render_template``, and + ``render_template_to_response`` have been removed. These have issued a + deprecation warning upon import since Pyramid 1.0. Use + :func:`pyramid.renderers.get_renderer`, + ``pyramid.renderers.get_renderer().implementation()``, + :func:`pyramid.renderers.render` or + :func:`pyramid.renderers.render_to_response` respectively instead of these + functions. + +- The ``pyramid.configuration`` module was removed. It had been deprecated + since Pyramid 1.0 and printed a deprecation warning upon its use. Use + :mod:`pyramid.config` instead. + +- The ``pyramid.paster.PyramidTemplate`` API was removed. It had been + deprecated since Pyramid 1.1 and issued a warning on import. If your code + depended on this, adjust your code to import + :class:`pyramid.scaffolds.PyramidTemplate` instead. + +- The ``pyramid.settings.get_settings()`` API was removed. It had been + printing a deprecation warning since Pyramid 1.0. If your code depended on + this API, use ``pyramid.threadlocal.get_current_registry().settings`` + instead or use the ``settings`` attribute of the registry available from + the request (``request.registry.settings``). + +- These APIs from the ``pyramid.testing`` module were removed. They have + been printing deprecation warnings since Pyramid 1.0: + + * ``registerDummySecurityPolicy``, use + :meth:`pyramid.config.Configurator.testing_securitypolicy` instead. + + * ``registerResources`` (aka ``registerModels``), use + :meth:`pyramid.config.Configurator.testing_resources` instead. + + * ``registerEventListener``, use + :meth:`pyramid.config.Configurator.testing_add_subscriber` instead. + + * ``registerTemplateRenderer`` (aka ``registerDummyRenderer``), use + :meth:`pyramid.config.Configurator.testing_add_renderer` instead. + + * ``registerView``, use :meth:`pyramid.config.Configurator.add_view` instead. + + * ``registerUtility``, use + :meth:`pyramid.config.Configurator.registry.registerUtility` instead. + + * ``registerAdapter``, use + :meth:`pyramid.config.Configurator.registry.registerAdapter` instead. + + * ``registerSubscriber``, use + :meth:`pyramid.config.Configurator.add_subscriber` instead. + + * ``registerRoute``, use + :meth:`pyramid.config.Configurator.add_route` instead. + + * ``registerSettings``, use + :meth:`pyramid.config.Configurator.add_settings` instead. + +- In Pyramid 1.3 and previous, the ``__call__`` method of a Response object + returned by a view was invoked before any finished callbacks were executed. + As of this release, the ``__call__`` method of a Response object is invoked + *after* finished callbacks are executed. This is in support of the + :meth:`pyramid.request.Request.invoke_subrequest` feature. + +Deprecations +------------ + +- The :meth:`pyramid.config.Configurator.set_request_property` directive has + been documentation-deprecated. The method remains usable but the more + featureful :meth:`pyramid.config.Configurator.add_request_method` should be + used in its place (it has all of the same capabilities but can also extend + the request object with methods). + +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` will emit a + deprecation warning if an application is using the policy without explicitly + passing a ``hashalg`` argument. This is because the default is "md5" which is + considered theoretically subject to collision attacks. If you really want + "md5" then you must specify it explicitly to get rid of the warning. + +Documentation Enhancements +-------------------------- + +- Added an :ref:`upgrading_chapter` chapter to the narrative documentation. + It describes how to cope with deprecations and removals of Pyramid APIs and + how to show Pyramid-generated deprecation warnings while running tests and + while running a server. + +- Added a :ref:`subrequest_chapter` chapter to the narrative documentation. + +- All of the tutorials that use + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` now explicitly + pass ``sha512`` as a ``hashalg`` argument. + +- Many cleanups and improvements to narrative and API docs. + +Dependency Changes +------------------ + +- Pyramid now requires WebOb 1.2b3+ (the prior Pyramid release only relied on + 1.2dev+). This is to ensure that we obtain a version of WebOb that returns + ``request.path_info`` as text. + diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst new file mode 100644 index 000000000..a477ce5ec --- /dev/null +++ b/docs/whatsnew-1.5.rst @@ -0,0 +1,526 @@ +What's New in Pyramid 1.5 +========================= + +This article explains the new features in :app:`Pyramid` version 1.5 as +compared to its predecessor, :app:`Pyramid` 1.4. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.5, as well as software dependency changes and notable +documentation additions. + +Major Backwards Incompatibilities +--------------------------------- + +- Pyramid no longer depends on or configures the Mako and Chameleon templating + system renderers by default. Disincluding these templating systems by + default means that the Pyramid core has fewer dependencies and can run on + future platforms without immediate concern for the compatibility of its + templating add-ons. It also makes maintenance slightly more effective, as + different people can maintain the templating system add-ons that they + understand and care about without needing commit access to the Pyramid core, + and it allows users who just don't want to see any packages they don't use + come along for the ride when they install Pyramid. + + This means that upon upgrading to Pyramid 1.5a2+, projects that use either + of these templating systems will see a traceback that ends something like + this when their application attempts to render a Chameleon or Mako template:: + + ValueError: No such renderer factory .pt + + Or:: + + ValueError: No such renderer factory .mako + + Or:: + + ValueError: No such renderer factory .mak + + Support for Mako templating has been moved into an add-on package named + ``pyramid_mako``, and support for Chameleon templating has been moved into + an add-on package named ``pyramid_chameleon``. These packages are drop-in + replacements for the old built-in support for these templating langauges. + All you have to do is install them and make them active in your configuration + to register renderer factories for ``.pt`` and/or ``.mako`` (or ``.mak``) to + make your application work again. + + To re-add support for Chameleon and/or Mako template renderers into your + existing projects, follow the below steps. + + If you depend on Mako templates: + + * Make sure the ``pyramid_mako`` package is installed. One way to do this + is by adding ``pyramid_mako`` to the ``install_requires`` section of your + package's ``setup.py`` file and afterwards rerunning ``setup.py develop``:: + + setup( + #... + install_requires=[ + 'pyramid_mako', # new dependency + 'pyramid', + #... + ], + ) + + * Within the portion of your application which instantiates a Pyramid + :class:`~pyramid.config.Configurator` (often the ``main()`` function in + your project's ``__init__.py`` file), tell Pyramid to include the + ``pyramid_mako`` includeme:: + + config = Configurator(.....) + config.include('pyramid_mako') + + If you depend on Chameleon templates: + + * Make sure the ``pyramid_chameleon`` package is installed. One way to do + this is by adding ``pyramid_chameleon`` to the ``install_requires`` section + of your package's ``setup.py`` file and afterwards rerunning + ``setup.py develop``:: + + setup( + #... + install_requires=[ + 'pyramid_chameleon', # new dependency + 'pyramid', + #... + ], + ) + + * Within the portion of your application which instantiates a Pyramid + :class:`~pyramid.config.Configurator` (often the ``main()`` function in + your project's ``__init__.py`` file), tell Pyramid to include the + ``pyramid_chameleon`` includeme:: + + config = Configurator(.....) + config.include('pyramid_chameleon') + + Note that it's also fine to install these packages into *older* Pyramids for + forward compatibility purposes. Even if you don't upgrade to Pyramid 1.5 + immediately, performing the above steps in a Pyramid 1.4 installation is + perfectly fine, won't cause any difference, and will give you forward + compatibility when you eventually do upgrade to Pyramid 1.5. + + With the removal of Mako and Chameleon support from the core, some + unit tests that use the ``pyramid.renderers.render*`` methods may begin to + fail. If any of your unit tests are invoking either + ``pyramid.renderers.render()`` or ``pyramid.renderers.render_to_response()`` + with either Mako or Chameleon templates then the + ``pyramid.config.Configurator`` instance in effect during + the unit test should be also be updated to include the addons, as shown + above. For example:: + + class ATest(unittest.TestCase): + def setUp(self): + self.config = pyramid.testing.setUp() + self.config.include('pyramid_mako') + + def test_it(self): + result = pyramid.renderers.render('mypkg:templates/home.mako', {}) + + Or:: + + class ATest(unittest.TestCase): + def setUp(self): + self.config = pyramid.testing.setUp() + self.config.include('pyramid_chameleon') + + def test_it(self): + result = pyramid.renderers.render('mypkg:templates/home.pt', {}) + +- If you're using the Pyramid debug toolbar, when you upgrade Pyramid to + 1.5a2+, you'll also need to upgrade the ``pyramid_debugtoolbar`` package to + at least version 1.0.8, as older toolbar versions are not compatible with + Pyramid 1.5a2+ due to the removal of Mako support from the core. It's + fine to use this newer version of the toolbar code with older Pyramids too. + +Feature Additions +----------------- + +The feature additions in Pyramid 1.5 follow. + +- Python 3.4 compatibility. + +- Add ``pdistreport`` script, which prints the Python version in use, the + Pyramid version in use, and the version number and location of all Python + distributions currently installed. + +- Add the ability to invert the result of any view, route, or subscriber + predicate value using the ``not_`` class. For example: + + .. code-block:: python + + from pyramid.config import not_ + + @view_config(route_name='myroute', request_method=not_('POST')) + def myview(request): ... + + The above example will ensure that the view is called if the request method + is not POST, at least if no other view is more specific. + + The :class:`pyramid.config.not_` class can be used against any value that is + a predicate value passed in any of these contexts: + + - :meth:`pyramid.config.Configurator.add_view` + + - :meth:`pyramid.config.Configurator.add_route` + + - :meth:`pyramid.config.Configurator.add_subscriber` + + - :meth:`pyramid.view.view_config` + + - :meth:`pyramid.events.subscriber` + +- View lookup will now search for valid views based on the inheritance + hierarchy of the context. It tries to find views based on the most specific + context first, and upon predicate failure, will move up the inheritance chain + to test views found by the super-type of the context. In the past, only the + most specific type containing views would be checked and if no matching view + could be found then a PredicateMismatch would be raised. Now predicate + mismatches don't hide valid views registered on super-types. Here's an + example that now works: + + .. code-block:: python + + class IResource(Interface): + + ... + + @view_config(context=IResource) + def get(context, request): + + ... + + @view_config(context=IResource, request_method='POST') + def post(context, request): + + ... + + @view_config(context=IResource, request_method='DELETE') + def delete(context, request): + + ... + + @implementer(IResource) + class MyResource: + + ... + + @view_config(context=MyResource, request_method='POST') + def override_post(context, request): + + ... + + Previously the override_post view registration would hide the get + and delete views in the context of MyResource -- leading to a + predicate mismatch error when trying to use GET or DELETE + methods. Now the views are found and no predicate mismatch is + raised. + See https://github.com/Pylons/pyramid/pull/786 and + https://github.com/Pylons/pyramid/pull/1004 and + https://github.com/Pylons/pyramid/pull/1046 + +- ``scripts/prequest.py`` (aka the ``prequest`` console script): added support + for submitting ``PUT`` and ``PATCH`` requests. See + https://github.com/Pylons/pyramid/pull/1033. add support for submitting + ``OPTIONS`` and ``PROPFIND`` requests, and allow users to specify basic + authentication credentials in the request via a ``--login`` argument to the + script. See https://github.com/Pylons/pyramid/pull/1039. + +- The :meth:`pyramid.config.Configurator.add_route` method now supports being + called with an external URL as pattern. See + https://github.com/Pylons/pyramid/issues/611 and the documentation section + :ref:`external_route_narr`. + +- :class:`pyramid.authorization.ACLAuthorizationPolicy` supports ``__acl__`` as + a callable. This removes the ambiguity between the potential + ``AttributeError`` that would be raised on the ``context`` when the property + was not defined and the ``AttributeError`` that could be raised from any + user-defined code within a dynamic property. It is recommended to define a + dynamic ACL as a callable to avoid this ambiguity. See + https://github.com/Pylons/pyramid/issues/735. + +- Allow a protocol-relative URL (e.g. ``//example.com/images``) to be passed to + :meth:`pyramid.config.Configurator.add_static_view`. This allows + externally-hosted static URLs to be generated based on the current protocol. + +- The :class:`pyramid.authentication.AuthTktAuthenticationPolicy` class has two + new options to configure its domain usage: + + * ``parent_domain``: if set the authentication cookie is set on + the parent domain. This is useful if you have multiple sites sharing the + same domain. + + * ``domain``: if provided the cookie is always set for this domain, bypassing + all usual logic. + + See https://github.com/Pylons/pyramid/pull/1028, + https://github.com/Pylons/pyramid/pull/1072 and + https://github.com/Pylons/pyramid/pull/1078. + +- The :class:`pyramid.authentication.AuthTktPolicy` now supports IPv6 + addresses when using the ``include_ip=True`` option. This is possibly + incompatible with alternative ``auth_tkt`` implementations, as the + specification does not define how to properly handle IPv6. See + https://github.com/Pylons/pyramid/issues/831. + +- Make it possible to use variable arguments via + :func:`pyramid.paster.get_appsettings`. This also allowed the generated + ``initialize_db`` script from the ``alchemy`` scaffold to grow support for + options in the form ``a=1 b=2`` so you can fill in values in a parameterized + ``.ini`` file, e.g. ``initialize_myapp_db etc/development.ini a=1 b=2``. + See https://github.com/Pylons/pyramid/pull/911 + +- The ``request.session.check_csrf_token()`` method and the ``check_csrf`` view + predicate now take into account the value of the HTTP header named + ``X-CSRF-Token`` (as well as the ``csrf_token`` form parameter, which they + always did). The header is tried when the form parameter does not exist. + +- You can now generate "hybrid" urldispatch/traversal URLs more easily by using + the new ``route_name``, ``route_kw`` and ``route_remainder_name`` arguments + to :meth:`~pyramid.request.Request.resource_url` and + :meth:`~pyuramid.request.Request.resource_path`. See + :ref:`generating_hybrid_urls`. + +- A new http exception superclass named + :class:`~pyramid.httpexceptions.HTTPSuccessful` was added. You can use this + class as the ``context`` of an exception view to catch all 200-series + "exceptions" (e.g. "raise HTTPOk"). This also allows you to catch *only* the + :class:`~pyramid.httpexceptions.HTTPOk` exception itself; previously this was + impossible because a number of other exceptions (such as ``HTTPNoContent``) + inherited from ``HTTPOk``, but now they do not. + +- It is now possible to escape double braces in Pyramid scaffolds (unescaped, + these represent replacement values). You can use ``\{\{a\}\}`` to + represent a "bare" ``{{a}}``. See + https://github.com/Pylons/pyramid/pull/862 + +- Add ``localizer`` and ``locale_name`` properties (reified) to + :class:`pyramid.request.Request`. See + https://github.com/Pylons/pyramid/issues/508. Note that the + :func:`pyramid.i18n.get_localizer` and :func:`pyramid.i18n.get_locale_name` + functions now simply look up these properties on the request. + +- The ``pserve`` command now takes a ``-v`` (or ``--verbose``) flag and a + ``-q`` (or ``--quiet``) flag. Output from running ``pserve`` can be + controlled using these flags. ``-v`` can be specified multiple times to + increase verbosity. ``-q`` sets verbosity to ``0`` unconditionally. The + default verbosity level is ``1``. + +- The ``alchemy`` scaffold tests now provide better coverage. See + https://github.com/Pylons/pyramid/pull/1029 + +- Users can now provide dotted Python names to as the ``factory`` argument + the Configurator methods named + :meth:`~pyramid.config.Configurator.add_view_predicate`, + :meth:`~pyramid.config.Configurator.add_route_predicate` and + :meth:`~pyramid.config.Configurator.add_subscriber_predicate`. Instead of + passing the predicate factory directly, you can pass a dotted name which + refers to the factory. + +- :func:`pyramid.path.package_name` no longer thows an exception when resolving + the package name for namespace packages that have no ``__file__`` attribute. + +- An authorization API has been added as a method of the request: + :meth:`pyramid.request.Request.has_permission`. It is a method-based + alternative to the :func:`pyramid.security.has_permission` API and works + exactly the same. The older API is now deprecated. + +- Property API attributes have been added to the request for easier access to + authentication data: :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.unauthenticated_userid`, and + :attr:`pyramid.request.Request.effective_principals`. These are analogues, + respectively, of :func:`pyramid.security.authenticated_userid`, + :func:`pyramid.security.unauthenticated_userid`, and + :func:`pyramid.security.effective_principals`. They operate exactly the + same, except they are attributes of the request instead of functions + accepting a request. They are properties, so they cannot be assigned to. + The older function-based APIs are now deprecated. + +- Pyramid's console scripts (``pserve``, ``pviews``, etc) can now be run + directly, allowing custom arguments to be sent to the python interpreter + at runtime. For example:: + + python -3 -m pyramid.scripts.pserve development.ini + +- Added a specific subclass of :class:`pyramid.httpexceptions.HTTPBadRequest` + named :class:`pyramid.exceptions.BadCSRFToken` which will now be raised in + response to failures in the ``check_csrf_token`` view predicate. See + https://github.com/Pylons/pyramid/pull/1149 + +- Added a new ``SignedCookieSessionFactory`` which is very similar to the + ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on + signing content. The custom serializer arguments to this function should + only focus on serializing, unlike its predecessor which required the + serializer to also perform signing. + See https://github.com/Pylons/pyramid/pull/1142 . Note + that cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using ``UnencryptedCookieSessionFactory``, + so existing user session data will be destroyed if you switch to it. + +- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie + factory that can be used by framework implementors to create their own + session implementations. It provides a reusable API which focuses strictly + on providing a dictionary-like object that properly handles renewals, + timeouts, and conformance with the ``ISession`` API. + See https://github.com/Pylons/pyramid/pull/1142 + +- We no longer eagerly clear ``request.exception`` and ``request.exc_info`` in + the exception view tween. This makes it possible to inspect exception + information within a finished callback. See + https://github.com/Pylons/pyramid/issues/1223. + + +Other Backwards Incompatibilities +--------------------------------- + +- Modified the :meth:`~pyramid.request.Reuqest.current_route_url` method. The + method previously returned the URL without the query string by default, it + now does attach the query string unless it is overriden. + +- The :meth:`~pyramid.request.Request.route_url` and + :meth:`~pyramid.request.Request.route_path` APIs no longer quote ``/`` to + ``%2F`` when a replacement value contains a ``/``. This was pointless, as + WSGI servers always unquote the slash anyway, and Pyramid never sees the + quoted value. + +- It is no longer possible to set a ``locale_name`` attribute of the request, + nor is it possible to set a ``localizer`` attribute of the request. These + are now "reified" properties that look up a locale name and localizer + respectively using the machinery described in :ref:`i18n_chapter`. + +- If you send an ``X-Vhm-Root`` header with a value that ends with any number + of slashes, the trailing slashes will be removed before the URL + is generated when you use :meth:`~pyramid.request.Request.resource_url` + or :meth:`~pyramid.request.Request.resource_path`. Previously the virtual + root path would not have trailing slashes stripped, which would influence URL + generation. + +- The :class:`pyramid.interfaces.IResourceURL` interface has now grown two new + attributes: ``virtual_path_tuple`` and ``physical_path_tuple``. These should + be the tuple form of the resource's path (physical and virtual). + +- Removed the ``request.response_*`` varying attributes (such + as``request.response_headers``) . These attributes had been deprecated + since Pyramid 1.1, and as per the deprecation policy, have now been removed. + +- ``request.response`` will no longer be mutated when using the + :func:`pyramid.renderers.render` API. Almost all renderers mutate the + ``request.response`` response object (for example, the JSON renderer sets + ``request.response.content_type`` to ``application/json``), but this is + only necessary when the renderer is generating a response; it was a bug + when it was done as a side effect of calling + :func:`pyramid.renderers.render`. + +- Removed the ``bfg2pyramid`` fixer script. + +- The :class:`pyramid.events.NewResponse` event is now sent **after** response + callbacks are executed. It previously executed before response callbacks + were executed. Rationale: it's more useful to be able to inspect the response + after response callbacks have done their jobs instead of before. + +- Removed the class named ``pyramid.view.static`` that had been deprecated + since Pyramid 1.1. Instead use :class:`pyramid.static.static_view` with the + ``use_subpath=True`` argument. + +- Removed the ``pyramid.view.is_response`` function that had been deprecated + since Pyramid 1.1. Use the :meth:`pyramid.request.Request.is_response` + method instead. + +- Removed the ability to pass the following arguments to + :meth:`pyramid.config.Configurator.add_route`: ``view``, ``view_context``. + ``view_for``, ``view_permission``, ``view_renderer``, and ``view_attr``. + Using these arguments had been deprecated since Pyramid 1.1. Instead of + passing view-related arguments to ``add_route``, use a separate call to + :meth:`pyramid.config.Configurator.add_view` to associate a view with a route + using its ``route_name`` argument. Note that this impacts the + :meth:`pyramid.config.Configurator.add_static_view` function too, because + it delegates to``add_route``. + +- Removed the ability to influence and query a :class:`pyramid.request.Request` + object as if it were a dictionary. Previously it was possible to use methods + like ``__getitem__``, ``get``, ``items``, and other dictlike methods to + access values in the WSGI environment. This behavior had been deprecated + since Pyramid 1.1. Use methods of ``request.environ`` (a real dictionary) + instead. + +- Removed ancient backwards compatibily hack in + ``pyramid.traversal.DefaultRootFactory`` which populated the ``__dict__`` of + the factory with the matchdict values for compatibility with BFG 0.9. + +- The ``renderer_globals_factory`` argument to the + :class:`pyramid.config.Configurator` constructor and the + coresponding argument to :meth:`~pyramid.config.Configurator.setup_registry` + has been removed. The ``set_renderer_globals_factory`` method of + :class:`~pyramid.config.Configurator` has also been removed. The (internal) + ``pyramid.interfaces.IRendererGlobals`` interface was also removed. These + arguments, methods and interfaces had been deprecated since 1.1. Use a + ``BeforeRender`` event subscriber as documented in the "Hooks" chapter of the + Pyramid narrative documentation instead of providing renderer globals values + to the configurator. + +- The key/values in the ``_query`` parameter of + :meth:`pyramid.request.Request.route_url` and the ``query`` parameter of + :meth:`pyramid.request.Request.resource_url` (and their variants), used to + encode a value of ``None`` as the string ``'None'``, leaving the resulting + query string to be ``a=b&key=None``. The value is now dropped in this + situation, leaving a query string of ``a=b&key=``. See + https://github.com/Pylons/pyramid/issues/1119 + +Deprecations +------------ + +- Returning a ``("defname", dict)`` tuple from a view which has a Mako renderer + is now deprecated. Instead you should use the renderer spelling + ``foo#defname.mak`` in the view configuration definition and return a dict + only. + +- The :meth:`pyramid.config.Configurator.set_request_property` method now issues + a deprecation warning when used. It had been docs-deprecated in 1.4 + but did not issue a deprecation warning when used. + +- :func:`pyramid.security.has_permission` is now deprecated in favor of using + :meth:`pyramid.request.Request.has_permission`. + +- The :func:`pyramid.security.authenticated_userid`, + :func:`pyramid.security.unauthenticated_userid`, and + :func:`pyramid.security.effective_principals` functions have been + deprecated. Use :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.unauthenticated_userid` and + :attr:`pyramid.request.Request.effective_principals` instead. + +- Deprecate the ``pyramid.interfaces.ITemplateRenderer`` interface. It was + ill-defined and became unused when Mako and Chameleon template bindings were + split into their own packages. + +- The ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` API has been + deprecated and is superseded by the + ``pyramid.session.SignedCookieSessionFactory``. Note that while the cookies + generated by the ``UnencryptedCookieSessionFactoryConfig`` + are compatible with cookies generated by old releases, cookies generated by + the SignedCookieSessionFactory are not. See + https://github.com/Pylons/pyramid/pull/1142 + +Documentation Enhancements +-------------------------- + +- A new documentation chapter named :ref:`quick_tour` was added. It describes + starting out with Pyramid from a high level. + +- Added a :ref:`quick_tutorial` to go with the Quick Tour + +- Many other enhancements. + +Scaffolding Enhancements +------------------------ + +- All scaffolds have a new HTML + CSS theme. + +- Updated docs and scaffolds to keep in step with new 2.0 release of + ``Lingua``. This included removing all ``setup.cfg`` files from scaffolds + and documentation environments. + +Dependency Changes +------------------ + +- Pyramid no longer depends upon ``Mako`` or ``Chameleon``. + +- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile`` + from 1.3+). diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst new file mode 100644 index 000000000..77d89b017 --- /dev/null +++ b/docs/whatsnew-1.6.rst @@ -0,0 +1,248 @@ +What's New in Pyramid 1.6 +========================= + +This article explains the new features in :app:`Pyramid` version 1.6 as +compared to its predecessor, :app:`Pyramid` 1.5. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.6, as well as software dependency changes and notable +documentation additions. + + +Backwards Incompatibilities +--------------------------- + +- IPython and BPython support have been removed from pshell in the core. To + continue using them on Pyramid 1.6+, you must install the binding packages + explicitly. One way to do this is by adding ``pyramid_ipython`` (or + ``pyramid_bpython``) to the ``install_requires`` section of your package's + ``setup.py`` file, then re-running ``setup.py develop``:: + + setup( + #... + install_requires=[ + 'pyramid_ipython', # new dependency + 'pyramid', + #... + ], + ) + +- ``request.response`` will no longer be mutated when using the + :func:`~pyramid.renderers.render_to_response` API. It is now necessary to + pass in a ``response=`` argument to + :func:`~pyramid.renderers.render_to_response` if you wish to supply the + renderer with a custom response object. If you do not pass one, then a + response object will be created using the current response factory. Almost + all renderers mutate the ``request.response`` response object (for example, + the JSON renderer sets ``request.response.content_type`` to + ``application/json``). However, when invoking ``render_to_response``, it is + not expected that the response object being returned would be the same one + used later in the request. The response object returned from + ``render_to_response`` is now explicitly different from ``request.response``. + This does not change the API of a renderer. See + https://github.com/Pylons/pyramid/pull/1563 + +- In an effort to combat a common issue it is now a + :class:`~pyramid.exceptions.ConfigurationError` to register a view + callable that is actually an unbound method when using the default view + mapper. As unbound methods do not exist in PY3+ possible errors are detected + by checking if the first parameter is named ``self``. For example, + `config.add_view(ViewClass.some_method, ...)` should actually be + `config.add_view(ViewClass, attr='some_method)'`. This was always an issue + in Pyramid on PY2 but the backward incompatibility is on PY3+ where you may + not use a function with the first parameter named ``self``. In this case + it looks too much like a common error and the exception will be raised. + See https://github.com/Pylons/pyramid/pull/1498 + + +Feature Additions +----------------- + +- Python 3.5 and pypy3 compatibility. + +- ``pserve --reload`` will no longer crash on syntax errors. See + https://github.com/Pylons/pyramid/pull/2044 + +- Cache busting for static resources has been added and is available via a new + :meth:`pyramid.config.Configurator.add_cache_buster` API. Core APIs are + shipped for both cache busting via query strings and via asset manifests for + integrating into custom asset pipelines. See + https://github.com/Pylons/pyramid/pull/1380 and + https://github.com/Pylons/pyramid/pull/1583 and + https://github.com/Pylons/pyramid/pull/2171 + +- Assets can now be overidden by an absolute path on the filesystem when using + the :meth:`~pyramid.config.Configurator.override_asset` API. This makes it + possible to fully support serving up static content from a mutable directory + while still being able to use the :meth:`~pyramid.request.Request.static_url` + API and :meth:`~pyramid.config.Configurator.add_static_view`. Previously it + was not possible to use :meth:`~pyramid.config.Configurator.add_static_view` + with an absolute path **and** generate urls to the content. This change + replaces the call, ``config.add_static_view('/abs/path', 'static')``, with + ``config.add_static_view('myapp:static', 'static')`` and + ``config.override_asset(to_override='myapp:static/', + override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely + made up and does not need to exist—it is used for generating URLs via + ``request.static_url('myapp:static/foo.png')``. See + https://github.com/Pylons/pyramid/issues/1252 + +- Added :meth:`~pyramid.config.Configurator.set_response_factory` and the + ``response_factory`` keyword argument to the constructor of + :class:`~pyramid.config.Configurator` for defining a factory that will return + a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 + +- Added :attr:`pyramid.config.Configurator.root_package` attribute and init + parameter to assist with includible packages that wish to resolve resources + relative to the package in which the configurator was created. This is + especially useful for add-ons that need to load asset specs from settings, in + which case it may be natural for a developer to define imports or assets + relative to the top-level package. See + https://github.com/Pylons/pyramid/pull/1337 + +- Overall improvements for the ``proutes`` command. Added ``--format`` and + ``--glob`` arguments to the command, introduced the ``method`` + column for displaying available request methods, and improved the ``view`` + output by showing the module instead of just ``__repr__``. See + https://github.com/Pylons/pyramid/pull/1488 + +- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server + URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533 + +- Support keyword-only arguments and function annotations in views in Python 3. + See https://github.com/Pylons/pyramid/pull/1556 + +- The ``append_slash`` argument of + :meth:`~pyramid.config.Configurator.add_notfound_view()` will now accept + anything that implements the :class:`~pyramid.interfaces.IResponse` interface + and will use that as the response class instead of the default + :class:`~pyramid.httpexceptions.HTTPFound`. See + https://github.com/Pylons/pyramid/pull/1610 + +- The :class:`~pyramid.config.Configurator` has grown the ability to allow + actions to call other actions during a commit cycle. This enables much more + logic to be placed into actions, such as the ability to invoke other actions + or group them for improved conflict detection. We have also exposed and + documented the configuration phases that Pyramid uses in order to further + assist in building conforming add-ons. See + https://github.com/Pylons/pyramid/pull/1513 + +- Allow an iterator to be returned from a renderer. Previously it was only + possible to return bytes or unicode. See + https://github.com/Pylons/pyramid/pull/1417 + +- Improve robustness to timing attacks in the + :class:`~pyramid.authentication.AuthTktCookieHelper` and the + :class:`~pyramid.session.SignedCookieSessionFactory` classes by using the + stdlib's ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ + and 3.3+). See https://github.com/Pylons/pyramid/pull/1457 + +- Improve the readability of the ``pcreate`` shell script output. See + https://github.com/Pylons/pyramid/pull/1453 + +- Make it simple to define ``notfound`` and ``forbidden`` views that wish to + use the default exception-response view, but with altered predicates and + other configuration options. The ``view`` argument is now optional in + :meth:`~pyramid.config.Configurator.add_notfound_view` and + :meth:`~pyramid.config.Configurator.add_forbidden_view` See + https://github.com/Pylons/pyramid/issues/494 + +- The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is + defined in the environment prior to launching the interpreter. See + https://github.com/Pylons/pyramid/pull/1448 + +- Add new HTTP exception objects for status codes ``428 Precondition + Required``, ``429 Too Many Requests`` and ``431 Request Header Fields Too + Large`` in ``pyramid.httpexceptions``. See + https://github.com/Pylons/pyramid/pull/1372/files + +- ``pcreate`` when run without a scaffold argument will now print information + on the missing flag, as well as a list of available scaffolds. See + https://github.com/Pylons/pyramid/pull/1566 and + https://github.com/Pylons/pyramid/issues/1297 + +- ``pcreate`` will now ask for confirmation if invoked with an argument for a + project name that already exists or is importable in the current environment. + See https://github.com/Pylons/pyramid/issues/1357 and + https://github.com/Pylons/pyramid/pull/1837 + +- Add :func:`pyramid.request.apply_request_extensions` function which can be + used in testing to apply any request extensions configured via + ``config.add_request_method``. Previously it was only possible to test the + extensions by going through Pyramid's router. See + https://github.com/Pylons/pyramid/pull/1581 + +- Make it possible to subclass ``pyramid.request.Request`` and also use + ``pyramid.request.Request.add_request.method``. See + https://github.com/Pylons/pyramid/issues/1529 + +- Additional shells for ``pshell`` can now be registered as entry points. See + https://github.com/Pylons/pyramid/pull/1891 and + https://github.com/Pylons/pyramid/pull/2012 + +- The variables injected into ``pshell`` are now displayed with their + docstrings instead of the default ``str(obj)`` when possible. See + https://github.com/Pylons/pyramid/pull/1929 + + +Deprecations +------------ + +- The ``pserve`` command's daemonization features, as well as + ``--monitor-restart``, have been deprecated. This includes the + ``[start,stop,restart,status]`` subcommands, as well as the ``--daemon``, + ``--stop-daemon``, ``--pid-file``, ``--status``, ``--user``, and ``--group`` + flags. See https://github.com/Pylons/pyramid/pull/2120 and + https://github.com/Pylons/pyramid/pull/2189 and + https://github.com/Pylons/pyramid/pull/1641 + + Please use a real process manager in the future instead of relying on + ``pserve`` to daemonize itself. Many options exist, including your operating + system's services, such as Systemd or Upstart, as well as Python-based + solutions like Circus and Supervisor. + + See https://github.com/Pylons/pyramid/pull/1641 and + https://github.com/Pylons/pyramid/pull/2120 + +- The ``principal`` argument to :func:`pyramid.security.remember` was renamed + to ``userid``. Using ``principal`` as the argument name still works and will + continue to work for the next few releases, but a deprecation warning is + printed. + + +Scaffolding Enhancements +------------------------ + +- Added line numbers to the log formatters in the scaffolds to assist with + debugging. See https://github.com/Pylons/pyramid/pull/1326 + +- Updated scaffold generating machinery to return the version of :app:`Pyramid` + and its documentation for use in scaffolds. Updated ``starter``, ``alchemy`` + and ``zodb`` templates to have links to correctly versioned documentation, + and to reflect which :app:`Pyramid` was used to generate the scaffold. + +- Removed non-ASCII copyright symbol from templates, as this was causing the + scaffolds to fail for project generation. + + +Documentation Enhancements +-------------------------- + +- Removed logging configuration from Quick Tutorial ``ini`` files, except for + scaffolding- and logging-related chapters, to avoid needing to explain it too + early. + +- Improve and clarify the documentation on what :app:`Pyramid` defines as a + ``principal`` and a ``userid`` in its security APIs. See + https://github.com/Pylons/pyramid/pull/1399 + +- Moved the documentation for ``accept`` on + :meth:`pyramid.config.Configurator.add_view` to no longer be part of the + predicate list. See https://github.com/Pylons/pyramid/issues/1391 for a bug + report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc + led to the conclusion that it should not be documented as a predicate. + See https://github.com/Pylons/pyramid/pull/1487 for this PR. + +- Clarify a previously-implied detail of the ``ISession.invalidate`` API + documentation. + +- Add documentation of command line programs (``p*`` scripts). See + https://github.com/Pylons/pyramid/pull/2191 diff --git a/docs/whatsnew-1.7.rst b/docs/whatsnew-1.7.rst new file mode 100644 index 000000000..fd144a24a --- /dev/null +++ b/docs/whatsnew-1.7.rst @@ -0,0 +1,172 @@ +What's New in Pyramid 1.7 +========================= + +This article explains the new features in :app:`Pyramid` version 1.7 as +compared to its predecessor, :app:`Pyramid` 1.6. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 1.7, as well as software dependency changes and notable +documentation additions. + +Backwards Incompatibilities +--------------------------- + +- The default hash algorithm for + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` has changed from + ``md5`` to ``sha512``. If you are using the authentication policy and need to + continue using ``md5``, please explicitly set ``hashalg='md5'``. + + If you are not currently specifying the ``hashalg`` option in your apps, then + this change means any existing auth tickets (and associated cookies) will no + longer be valid, users will be logged out, and have to login to their + accounts again. + + This change has been issuing a DeprecationWarning since :app:`Pyramid` 1.4. + + See https://github.com/Pylons/pyramid/pull/2496 + +- Python 2.6 and 3.2 are no longer supported by Pyramid. See + https://github.com/Pylons/pyramid/issues/2368 and + https://github.com/Pylons/pyramid/pull/2256 + +- The :func:`pyramid.session.check_csrf_token` function no longer validates a + csrf token in the query string of a request. Only headers and request bodies + are supported. See https://github.com/Pylons/pyramid/pull/2500 + +Feature Additions +----------------- + +- A new :ref:`view_derivers` concept has been added to Pyramid to allow + framework authors to inject elements into the standard Pyramid view pipeline + and affect all views in an application. This is similar to a decorator except + that it has access to options passed to ``config.add_view`` and can affect + other stages of the pipeline such as the raw response from a view or prior + to security checks. See https://github.com/Pylons/pyramid/pull/2021 + +- Added a new setting, ``pyramid.require_default_csrf`` which may be used + to turn on CSRF checks globally for every request in the application. + This should be considered a good default for websites built on Pyramid. + It is possible to opt-out of CSRF checks on a per-view basis by setting + ``require_csrf=False`` on those views. + See :ref:`auto_csrf_checking` and + https://github.com/Pylons/pyramid/pull/2413 + +- Added a ``require_csrf`` view option which will enforce CSRF checks on + requests with an unsafe method as defined by RFC2616. If the CSRF check fails + a ``BadCSRFToken`` exception will be raised and may be caught by exception + views (the default response is a ``400 Bad Request``). This option should be + used in place of the deprecated ``check_csrf`` view predicate which would + normally result in unexpected ``404 Not Found`` response to the client + instead of a catchable exception. See :ref:`auto_csrf_checking`, + https://github.com/Pylons/pyramid/pull/2413 and + https://github.com/Pylons/pyramid/pull/2500 + +- Added an additional CSRF validation that checks the origin/referrer of a + request and makes sure it matches the current ``request.domain``. This + particular check is only active when accessing a site over HTTPS as otherwise + browsers don't always send the required information. If this additional CSRF + validation fails a ``BadCSRFOrigin`` exception will be raised and may be + caught by exception views (the default response is ``400 Bad Request``). + Additional allowed origins may be configured by setting + ``pyramid.csrf_trusted_origins`` to a list of domain names (with ports if on + a non standard port) to allow. Subdomains are not allowed unless the domain + name has been prefixed with a ``.``. See + https://github.com/Pylons/pyramid/pull/2501 + +- Added a new :func:`pyramid.session.check_csrf_origin` API for validating the + origin or referrer headers against the request's domain. + See https://github.com/Pylons/pyramid/pull/2501 + +- Subclasses of :class:`pyramid.httpexceptions.HTTPException` will now take + into account the best match for the clients ``Accept`` header, and depending + on what is requested will return ``text/html``, ``application/json`` or + ``text/plain``. The default for ``*/*`` is still ``text/html``, but if + ``application/json`` is explicitly mentioned it will now receive a valid + JSON response. See https://github.com/Pylons/pyramid/pull/2489 + +- A new event, :class:`pyramid.events.BeforeTraversal`, and interface + :class:`pyramid.interfaces.IBeforeTraversal` have been introduced that will + notify listeners before traversal starts in the router. + See :ref:`router_chapter` as well as + https://github.com/Pylons/pyramid/pull/2469 and + https://github.com/Pylons/pyramid/pull/1876 + +- A new method, :meth:`pyramid.request.Request.invoke_exception_view`, which + can be used to invoke an exception view and get back a response. This is + useful for rendering an exception view outside of the context of the + ``EXCVIEW`` tween where you may need more control over the request. + See https://github.com/Pylons/pyramid/pull/2393 + +- Allow a leading ``=`` on the key of the request param predicate. + For example, ``'=abc=1'`` is equivalent down to + ``request.params['=abc'] == '1'``. + See https://github.com/Pylons/pyramid/pull/1370 + +- Allow using variable substitutions like ``%(LOGGING_LOGGER_ROOT_LEVEL)s`` + for logging sections of the .ini file and populate these variables from + the ``pserve`` command line -- e.g.: + + ``pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG`` + + This support is thanks to the new ``global_conf`` option on + :func:`pyramid.paster.setup_logging`. + See https://github.com/Pylons/pyramid/pull/2399 + +Deprecations +------------ + +- The ``check_csrf`` view predicate has been deprecated. Use the + new ``require_csrf`` option or the ``pyramid.require_default_csrf`` setting + to ensure that the :class:`pyramid.exceptions.BadCSRFToken` exception is + raised. See https://github.com/Pylons/pyramid/pull/2413 + +- Support for Python 3.3 will be removed in Pyramid 1.8. + https://github.com/Pylons/pyramid/issues/2477 + +Scaffolding Enhancements +------------------------ + +- A complete overhaul of the ``alchemy`` scaffold to show more modern best + practices with regards to SQLAlchemy session management, as well as a more + modular approach to configuration, separating routes into a separate module + to illustrate uses of :meth:`pyramid.config.Configurator.include`. + See https://github.com/Pylons/pyramid/pull/2024 + +Documentation Enhancements +-------------------------- + +A massive overhaul of the packaging and tools used in the documentation +was completed in https://github.com/Pylons/pyramid/pull/2468. A summary +follows: + +- All docs now recommend using ``pip`` instead of ``easy_install``. + +- The installation docs now expect the user to be using Python 3.4 or + greater with access to the ``python3 -m venv`` tool to create virtual + environments. + +- Tutorials now use ``py.test`` and ``pytest-cov`` instead of ``nose`` and + ``coverage``. + +- Further updates to the scaffolds as well as tutorials and their src files. + +Along with the overhaul of the ``alchemy`` scaffold came a total overhaul +of the :ref:`bfg_sql_wiki_tutorial` tutorial to introduce more modern +features into the usage of SQLAlchemy with Pyramid and provide a better +starting point for new projects. See +https://github.com/Pylons/pyramid/pull/2024 for more. Highlights were: + +- New SQLAlchemy session management without any global ``DBSession``. Replaced + by a per-request ``request.dbsession`` property. + +- A new authentication chapter demonstrating how to get simple authentication + bootstrapped quickly in an application. + +- Authorization was overhauled to show the use of per-route context factories + which demonstrate object-level authorization on top of simple group-level + authorization. Did you want to restrict page edits to only the owner but + couldn't figure it out before? Here you go! + +- The users and groups are stored in the database now instead of within + tutorial-specific global variables. + +- User passwords are stored using ``bcrypt``. |
