changeset 60:682cd33e564c draft

Documentation (incomplete).
author David Barts <n5jrn@me.com>
date Sat, 08 Jun 2019 07:43:15 -0700
parents 60907204a265
children 55828c01e38f
files doc/500.png doc/Makefile doc/conf.py doc/essential_characteristics.rst doc/hello1.png doc/hello2.png doc/index.rst doc/introduction.rst doc/make.bat doc/philosophy.rst doc/tutorial.rst
diffstat 11 files changed, 570 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
Binary file doc/500.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/Makefile	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/conf.py	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,52 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'TinCan'
+copyright = '2019, David Barts'
+author = 'David Barts'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# 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']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/essential_characteristics.rst	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,16 @@
+=========================
+Essential Characteristics
+=========================
+
+Peaceful Coexistence
+    A TinCan webapp is a Bottle webapp. You can also do things the Bottle
+    way… in the same webapp! Bottle-style webapps have access to the same
+    Chameleon templating engine that TinCan uses.
+    
+No Configuration
+    Just make a tree appropriately-named files, and TinCan will figure it
+    all out. The file tree defines the routes, much like in a web server.
+    
+Header Directives Can Get You a Lot
+    Need to have one controller associated with multiple views? TinCan
+    can do that! See the ``#python`` header directive.
\ No newline at end of file
Binary file doc/hello1.png has changed
Binary file doc/hello2.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/index.rst	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,29 @@
+.. TinCan documentation master file, created by
+   sphinx-quickstart on Tue Jun  4 20:41:00 2019.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to TinCan's documentation!
+==================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+   
+   introduction
+   philosophy
+   motives
+   essential_characteristics
+   tutorial
+   configuration
+   deployment
+   api_reference
+   command_reference
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/introduction.rst	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,8 @@
+************
+Introduction
+************
+
+TinCan is a Python web framework, based on the Bottle micro-framework and
+the Chameleon templating engine. TinCan is based on the "code-behind"
+paradigm popularized by ASP.Net, convention over configuration, and clear
+separation of presentation and back-end logic.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/make.bat	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/philosophy.rst	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,109 @@
+**********
+Philosophy
+**********
+
+=================
+Be WSGI Compliant
+=================
+
+It's the default standard for Python web frameworks.
+Only a fool wouldn't support it.
+
+========================
+Don’t Reinvent the Wheel
+========================
+
+TinCan is mostly Bottle and Chameleon. Why rewrite the basic guts of an
+application server, when they’ve already been written many times? Why
+write a new templating engine, when there’s existing ones out there?
+TinCan tries to leverage existing software as much as possible.
+
+======================
+Keep It Simple, Stupid
+======================
+
+I wanted to get on with the business of actually *using* TinCan to do things,
+so I didn't clutter it up with extra features that would be difficult to
+implement. (That's why there's currently no way to choose alternate templating engines
+to Chameleon. I tried to do that, and it proved overly complex. Maybe some day.)
+
+===========================
+Don't Be Gratuitously Bossy
+===========================
+
+TinCan is not tightly coupled with an ORM, and never will be. I personally dislike
+ORM's, and
+`I am not alone <http://seldo.com/weblog/2011/06/15/orm_is_an_antipattern>`_.
+Moreover, not all web applications need to use an SQL database in the first
+place.
+
+This is of no loss, even for those who prefer to use an ORM, since
+it is easy enough use an ORM with TinCan (just add it to the script you make
+to load the webapp, and include the ORM in the webapp's ``config`` dictionary).
+
+===========
+Code-Behind
+===========
+
+It's maligned by many, but I personally like it, and it
+does *not* necessarily mean repudiating the MVC paradigm.
+
+=====================
+Don't Repeat Yourself
+=====================
+
+One of the things I dislike about most Python web
+frameworks is that I'm always tiresomely repeating "this is route foo, serviced
+by controller foo \(which defines variables bar, baz and blort\), which uses template foo
+\(which renders variables bar, baz and blort\)." This is completely unnecessary;
+the relationships should be obvious by virtue of the similar names. The framework should figure
+it out and just "do the right thing" by default.
+
+============================
+MVC Comes in Different Forms
+============================
+
+If you show a little discipline, the template page will describe how data
+is presented to the user, and how he or she can interact with it. A view
+by any other name is still a view. The code-behind will tie that view into
+the underlying business logic, and bridge the gap between the two. A controller
+by any other name is still a controller.
+
+If you don't show discipline, you can clutter your templates up with bits
+of Python code that represent controller logic, and clutter your controller up with
+bits of presentation details. Of course you can. That's not the framework's
+fault; that's the programmer's fault. Bad code can be written in any language,
+for any platform.
+
+==================================================
+The Alternative is PHP or CGI, not Django or Flask
+==================================================
+
+I'm not the only one out there who doesn't like all the repeating yourself
+that most MVC frameworks make you do. There's still *a lot* of developers
+out there writing CGI scripts or using PHP, because CGI and PHP don't make
+you do all the busywork that frameworks do. (Can they be blamed? It really
+feels silly when all you want is to get a quick, small site up.)
+
+That's a pity, because CGI is hideously inefficient and PHP is, well, just
+plain hideous. \(A root namespace with literally *thousands* of builtins
+cluttering it up, builtins with *no* consistent naming convention? Ugh!\)
+
+Hopefully, by making it easier for people to code efficient web sites using
+a well-designed language that's easy to learn, I'll encourage more people
+to create efficient sites in a well-designed language. If not, well, at least
+I'll encourage *myself* to do so.
+
+============================================
+Meaningful Error Messages (If You Want Them)
+============================================
+
+Bottle can be notoriously tight-lipped about what's going wrong if and
+when things go wrong. (And when you're developing and debugging code,
+things go wrong). TinCan tries as hard as possible to catch errors and
+log them. Enable logging, and when you see a 500 error, you will almost
+always have a log file with a traceback in it helping to pinpoint the
+cause.
+
+That is, assuming you enable logging. By default, it is turned off, simply
+because WSGI provides no default place for errors to go.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tutorial.rst	Sat Jun 08 07:43:15 2019 -0700
@@ -0,0 +1,301 @@
+********
+Tutorial
+********
+
+.. highlight:: none
+
+==============================================
+First Step: Create a Directory for Your Webapp
+==============================================
+
+TinCan is a lot like a web server; it serves a directory of files. There is
+one catch, though: that directory must contain a ``WEB-INF`` subdirectory.
+It's OK if ``WEB-INF`` ends up being empty (as it can, for a really simple
+webapp). If ``WEB-INF`` is missing, TinCan will conclude the directory
+doesn't contain a webapp and will refuse to serve it. (This is a deliberate
+feature, added to prevent serving directories that don't contain webapps.)
+
+So the first thing we must do is create a directory to hold our new webapp.
+Let's call it ``demo``::
+    $ mkdir demo demo/WEB-INF
+
+===================
+Adding Some Content
+===================
+Use your favorite editor to create a file called ``hello.pspx``, and insert
+the following content into it::
+
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <title>Hello</title>
+      </head>
+      <body>
+        <h1>Hello, World</h1>
+        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
+      </body>
+    </html>
+
+Save the file to disk, and start up a test server::
+
+    $ launch demo
+
+You should see something like the following::
+
+    adding route: /hello.pspx (GET)
+    Bottle v0.12.16 server starting up (using WSGIRefServer())...
+    Listening on http://localhost:8080/
+    Hit Ctrl-C to quit.
+
+If you already have a server listening on port 8080, this naturally won't work.
+Use the ``--port`` option to tell ``launch`` to listen on another port, e.g.::
+
+    $ launch --port=8000
+
+When you have a working test server, point your browser at
+``http://localhost:8080/hello.pspx``
+and you should see something like the following:
+
+.. image:: hello1.png
+
+What just happened?
+
+#. TinCan found the ``hello.pspx`` file.
+
+#. In that file, it found no special header directives. It also found no ``hello.py`` file.
+
+#. So TinCan fell back to its default behavior, and created an instance of the ``tincan.Page`` class (the base class of all code-behind logic), and associated the template code in ``hello.pspx`` with it.
+
+#. The ``${page.request.environ['PATH_INFO']}`` expression references objects found in that standard page object to return the part of the HTTP request path that is being interpreted as a route within the webapp itself.
+
+==============================
+Adding Some Content of Our Own
+==============================
+
+Use Control-C to force the test server to quit, then add two more files. First,
+a template, ``hello2.pspx``::
+
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <title>Hello</title>
+      </head>
+      <body>
+        <h1>Hello Again, World</h1>
+        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
+        <p>The current time on the server is ${time}.</p>
+      </body>
+    </html>
+
+Next, some code-behind in ``hello2.py``::
+
+    import time
+    import tincan
+
+    class Hello2(tincan.Page):
+        def handle(self):
+            self.time = time.ctime()
+
+This time, when you launch the test server, you should notice a second route
+being created::
+
+    $ ./launch demo
+    adding route: /hello.pspx (GET)
+    adding route: /hello2.pspx (GET)
+    Bottle v0.12.16 server starting up (using WSGIRefServer())...
+    Listening on http://localhost:8080/
+    Hit Ctrl-C to quit.
+
+When you visit the new page, you should see something like this:
+
+.. image:: hello2.png
+
+What new happened this time?
+
+#. You created a code-behind file with the same name as its associated template (only the extensions differ).
+
+#. Because the file names match, TinCan deduced that the two files were related, one containing the code-behind for the associated template.
+
+#. TinCan looked in the code-behind file for a something subclassing ``tincan.Page``, and created an instance of that class, complete with the standard ``request`` and ``response`` instance variables.
+
+#. It then called the ``handle(self)`` method of that class, which defined yet another instance variable.
+
+#. Because that instance variable *did not* start with an underscore, TinCan considered it to be exportable, and exported it as a template variable.
+
+Suppose you had written the following code-behind instead::
+
+    from time import ctime
+    from tincan import Page
+
+    class Hello2(Page):
+        def handle(self):
+            self.time = ctime()
+
+Every class is a subclass of itself, so what stops TinCan from getting all confused now that there are two identifiers in this module, ``Page`` and ``Hello2``, both of which are subclasses of ``tincan.Page``? For that matter, what if you had defined your own subclass of ``tincan.Page`` and subclassed it further, e.g.::
+
+    from time import ctime
+    from mymodule import MyPage
+
+    class Hello2(MyPage):
+        def handle(self):
+            self.time = ctime()
+
+The answer is that the code would still have worked (you might want to try the first example above just to prove it). When deciding what class to use, TinCan looks for *the deepest subclass of tincan.Page it can find* in the code-behind. So in a code-behind file that contains both a reference to ``tincan.Page`` itself, and a subclass of ``tincan.Page``, the subclass will always "win." Likewise, a subclass of a subclass will always "win" over a direct subclass of the parent class.
+
+A code-behind file need not share the same file name as its associated
+template. If you use the ``#python`` header directive, you can tell TinCan
+exactly which code-behind file to use. For example::
+
+    #python other.py
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <title>Hello</title>
+      </head>
+      <body>
+        <h1>Hello Again, World</h1>
+        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
+        <p>The current time on the server is ${time}.</p>
+      </body>
+    </html>
+
+What happens if you edit ``hello2.pspx`` to look like the above but *do not* rename ``hello2.py`` to ``other.py``? What happens if you make ``hello2.pspx`` run with ``hello.py`` (and vice versa)? Try it and see!
+
+\(Note that it is generally not a good idea to gratuitously name things inconsistently, as it makes it hard for other people — or even you, at a later time — to easily figure out what is going on.\)
+
+===========
+Error Pages
+===========
+
+If you tried the exercises in the paragraph immediately above, some of them let you see what happens when something goes wrong in TinCan. What if that standard error page is not to your liking, and you want to display something customized? TinCan lets you do that. Create a file called ``500.pspx``::
+
+    #errors 500
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <title>${error.status_line}</title>
+      </head>
+      <body>
+        <h1>${error.status_line}</h1>
+        <p>How embarrassing! It seems there was an internal error serving
+        <kbd>${request.url}</kbd></p>
+      </body>
+    </html>
+
+Now when you visit a page that doesn't work due to server-side errors (such as a template referencing an undefined variable), you'll see something like:
+
+.. image:: 500.png
+
+Error pages work a little differently from normal pages. They are based on subclasses of ``tincan.ErrorPage``. You get two standard template variables "for free:" ``error``, containing an instance of a ``bottle.HTTPError`` object pertaining to the error, and ``request``, a ``bottle.HTTPRequest`` object pertaining to the request that triggered the error.
+
+It is not as common for error pages to have code-behind as it is for normal, non-error pages, but it is possible and allowed. As alluded to above, the class to subclass is ``tincan.ErrorPage``; such classes have a ``handle()`` method which may define instance variables which get exported to the associated template using the same basic rules as for normal pages.
+
+If an error page itself causes an error, TinCan (the underlying Bottle framework, actually) will ignore it and  display a fallback error page. This is to avoid triggering an infinite loop.
+
+====================================
+Index Pages and Server-Side Forwards
+====================================
+
+Remove any ``#python`` header directives you added to ``hello.pspx`` and ``hello2.pspx`` during your previous experiments and create ``index.pspx``::
+
+    #forward hello.pspx
+
+That's it, just one line! When you launch the modified webapp, you should notice a couple new routes being created::
+
+    $ launch demo
+    adding route: /hello.pspx (GET)
+    adding route: /hello2.pspx (GET)
+    adding route: /index.pspx (GET)
+    adding route: / (GET)
+    Bottle v0.12.16 server starting up (using WSGIRefServer())...
+    Listening on http://localhost:8080/
+    Hit Ctrl-C to quit.
+
+And if you navigate to ``http://localhost:8080/``, you should see our old friend the first page we created in this tutorial.
+
+Just like how a web server recognizes ``index.html`` as special, and routes requests for its containing directory to it, TinCan recognizes ``index.pspx`` as special.
+
+``#forward`` creates what is known as a *server-side forward*. Unlike with an HTTP forward, all the action happens on the server side. The client never sees any intermediate 3xx response and never has to make a second HTTP request to resolve the URL. Any TinCan template file containing a ``#forward`` header directive will have *everything else in it* ignored, and otherwise act as if it were an exact copy of the route referenced in the ``#forward`` header.
+
+It is permitted for a page to ``#forward`` to another ``#forward`` page (but you probably should think twice before doing this). ``#forward`` loops are not permitted and attempts to create them will be rejected and cause an error message at launch time.
+
+=====================================
+Form Data and Requests Other Than GET
+=====================================
+
+As a final example, here's a sample page, ``name.pspx``, that processes form data via a POST request, and which uses some `Chameleon TAL <https://chameleon.readthedocs.io/en/latest/reference.html#basics-tal>`_ to render and style the response page, call it::
+
+    #rem A page that accepts both GET and POST requests
+    #methods GET POST
+    <!DOCTYPE html>
+    <html>
+      <head>
+        <title>Form Test</title>
+      </head>
+      <body>
+        <h1>Form Test</h1>
+        <h2>Enter Your Name Below</h2>
+        <form method="post">
+          <input type="text" name="name"/>
+          <input type="submit" value="Submit"/>
+        </form>
+        <if tal:condition="message is not None" tal:omit-tag="True">
+          <h2 tal:content="message.subject"></h2>
+          <p tal:attributes="style message.style" tal:content="message.body"></p>
+        </if>
+      </body>
+    </html>
+
+Here's the code-behind for that page::
+
+    from tincan import Page
+    from jsdict import JSDict
+
+    class Name(Page):
+        def handle(self):
+            self._error_message = JSDict({"subject": "Error", "style": "color: red;"})
+            if "name" not in self.request.forms:
+                if self.request.method == "GET":
+                    self.message = None
+                else:
+                    self.message = self.error("This should not happen!")
+            else:
+                name = self.request.forms["name"]
+                if name.strip() == "":
+                    self.message = self.error("Please enter your name above.")
+                else:
+                    self.message = JSDict({
+                        "subject": "Hello, {0}".format(name.split()[0]),
+                        "style": None,
+                        "body": "Pleased to meet you!"
+                    })
+
+        def error(self, message):
+            self._error_message.body = message
+            return self._error_message
+
+This example requires you to create a third file, in ``WEB-INF/lib/jsdict.py``::
+
+    class JSDict(dict):
+        def __getattr__(self, name):
+            return self[name]
+
+        def __setattr__(self, name, value):
+            self[name] = value
+
+        def __delattr__(self, name):
+            del self[name]
+
+``WEB-INF/lib`` is the directory where webapp-specific library routines live. It gets added to ``sys.path`` automatically when your webapp is launched.
+
+This example introduces two new header directives, ``#rem`` and ``#methods``. The former introduces a remark (comment); the latter specifies the request methods that this page will respond to. Not specifying a ``#methods`` line is equivalent to specifying ``#methods GET``. When you launch the webapp after adding this new page, you shound notice TinCan announce that it is creating a route that supports both GET and POST requests for it::
+
+    $ launch demo
+    adding route: /hello.pspx (GET)
+    adding route: /hello2.pspx (GET)
+    adding route: /index.pspx (GET)
+    adding route: / (GET)
+    adding route: /name.pspx (GET,POST)
+    Bottle v0.12.16 server starting up (using WSGIRefServer())...
+    Listening on http://localhost:8080/
+    Hit Ctrl-C to quit.