<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
 "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
  [
    <!ENTITY module      "<code>birdnotes.py</code>">
    <!ENTITY rnc         "<code>birdnotes.rnc</code>">
    <!ENTITY selfURL     "http://www.nmt.edu/~shipman/aba/doc/pyims/">
    <!ENTITY spec        "http://www.nmt.edu/~shipman/aba/doc/">
    <!ENTITY xTestFile   "2999-01.xml">
    <!ENTITY identitest  "identitest">
    <!ENTITY treetest    "treetest">
  ]
>
<article>
  <articleinfo>
    <title>&module;: Objects to represent bird note files</title>
    <titleabbrev>&module;</titleabbrev>
    <authorgroup>
      <author>
        <firstname>John W.</firstname>
        <surname>Shipman</surname>
      </author>
    </authorgroup>
    <address><email>john@nmt.edu</email>
    </address>
    <revhistory>
      <revision>
        <revnumber>$Revision: 1.47 $</revnumber>
        <date>$Date: 2010/03/21 01:16:55 $</date>
      </revision>
    </revhistory>
    <abstract>
      <para>
        This document describes the internals of a
        Python-language module to represent birdwatching field
        notes in XML.        
      </para>
      <para>
        This publication is available in <ulink url="&selfURL;"
        >Web form</ulink > and also as a <ulink
        url="&selfURL;birdnotespy.pdf" >PDF document</ulink >.  Please
        forward any comments to <userinput
        >john@nmt.edu</userinput >.
      </para>
    </abstract>
  </articleinfo>
  <section id='overview'>
    <title>Overview</title>
    <para>
      <ulink url='http://www.nmt.edu/~shipman/aba/doc/'
      ><citetitle >A system for encoding bird field
      notes</citetitle ></ulink > describes an XML schema for
      encoding birdwatching field notes, along with a Python
      module that reads and writes XML files conforming to that
      schema.  This document contains the actual code for the
      module, in literate programming style.  For more
      information, see the author's <ulink
      url='http://www.nmt.edu/~shipman/soft/litprog/' >literate
      programming page</ulink >.
    </para>
    <para>
      The &module; module reads and writes XML using techniques
      described in <ulink
      url='http://www.nmt.edu/tcc/help/pubs/pylxml/' ><citetitle
      >Python XML processing with <code >lxml</code ></citetitle
      ></ulink >.  The reader should be familiar with Python and
      XML.
    </para>
    <para>
      Files referred to in this document:
    </para>
    <itemizedlist spacing="compact">
      <listitem>
        <para>
          The module itself, <ulink url='&selfURL;birdnotes.py'
          ><filename >birdnotes.py</filename ></ulink >.
        </para>
      </listitem>
      <listitem>
        <para>
          The DocBook source for this document, <ulink
          url='&selfURL;birdnotespy.xml' ><filename
          >birdnotespy.xml</filename ></ulink >.
        </para>
      </listitem>
    </itemizedlist>
    <para>
      The module exports a number of classes.  Each represents
      an XML element.
    </para>
    <itemizedlist>
      <listitem>
        <para>
          <code >class BirdNoteSet</code > represents the
          contents of one XML file conforming to &rnc;.
          See <xref linkend='class-BirdNoteSet' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-DayNotes' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-DaySummary' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Loc' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Gps' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-BirdForm' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Sighting' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-LocGroup' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-AgeSexGroup' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-SightNotes' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Photo' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Narrative' />.
        </para>
      </listitem>
      <listitem>
        <para>
          <xref linkend='class-Paragraph' />.
        </para>
      </listitem>
    </itemizedlist>
  </section> <!--overview-->
  <section id='prologue'>
    <title>Module prologue</title>
    <para>
      The code starts with a Python module documentation string
      that refers the reader of the code back to this document.
    </para>
    <programlisting role='outFile:birdnotes.py'
>"""&module;: Represents an XML file conforming to &rnc;

    For documentation, see:
      &selfURL;      
"""
</programlisting>
  </section> <!--prologue-->
  <section id='imports'>
    <title>Module imports</title>
    <para>
      This module is built on top of a sizeable collection of
      other modules.  Here are their imports.
    </para>
    <para>
      We'll need the standard Python <code >sys</code > module for
      access to standard I/O streams.  We also need <code >os</code >
      and <code >stat</code > to retrieve file timestamps, <code
      >datetime</code > for calendar functions, and the standard
      regular expression library <code >re</code >.
    </para>
    <programlisting role='outFile:birdnotes.py'
>#================================================================
# Imports
#----------------------------------------------------------------
import sys, os, stat, datetime, re
</programlisting>
    <para>
      XML processing is done using the <code >lxml.etree</code >
      package.  We call this module <code >et</code > here.
    </para>
    <programlisting role='outFile:birdnotes.py'
>from lxml import etree as et
</programlisting>
    <para>
      Because the module manipulates GPS waypoints, we next
      import a part of the author's mapping package, <filename
      >terrapos.py</filename >.  See the documentation, <ulink
      url='http://infohost.nmt.edu/tcc/help/lang/python/mapping/doc/'
      ><citetitle >A Python mapping package</citetitle ></ulink
      >.
    </para>
    <programlisting role='outFile:birdnotes.py'
>import terrapos
</programlisting>
    <para>
      The next module is a vital part of the bird records system:
      the taxonomy module.  See the documentation: <ulink
      url='http://www.nmt.edu/~shipman/xnomo/' ><citetitle >A
      system for representing bird taxonomy</citetitle ></ulink
      >.  Module <filename >txny.py</filename > is the main
      taxonomy module.  Module <filename >abbr.py</filename >
      contains auxiliary functions for handling general bird
      identifications including species pairs and hybrids.
    </para>
    <programlisting role='outFile:birdnotes.py'
>import txny
import abbr as abbrModule
</programlisting>
    <para>
      Rather than use string constants for the names of the XML
      elements and attributes in the <code >birdnotes.rnc</code >
      schema, we use a script named <application
      >pyrang</application > to extract those names and write a
      file named <filename >rnc.py</filename >
      containing declarations of symbolic names for each one.
      For more information, see <ulink
      url='http://www.nmt.edu/tcc/help/lang/python/examples/pyrang/'
      >the documentation for <application >pyrang</application
      ></ulink >.  See also the generated <ulink
      url='http://www.nmt.edu/~shipman/aba/doc/pyims/rnc.py'
      >actual <filename >rnc.py</filename > file</ulink
      >.
    </para>
    <programlisting role='outFile:birdnotes.py'
>import rnc
</programlisting>
  </section> <!--imports-->
  <section id='constants'>
    <title>Manifest constants</title>
    <para>
      Constants used throughout the module.
    </para>
    <section id='SCHEMA_RNG'>
      <title><code >SCHEMA_RNG</code >: Schema file name</title>
      <para>
        In order to validate XML files against <ulink
        url='&spec;' >the schema</ulink >, we need read access to
        the schema file in Relax NG form, that is, an XML
        <filename >.rng</filename > file, not in the <filename
        >.rnc</filename > compact form.  To translate an <code
        >.rnc</code > schema into <filename >.rng</filename >
        form, use the freely available <ulink
        url='http://www.thaiopensource.com/relaxng/trang.html'
        ><application >trang</application ></ulink > multi-format
        schema converter.
      </para>
      <programlisting role='outFile:birdnotes.py'
>#================================================================
# Manifest constants
#----------------------------------------------------------------

SCHEMA_RNG  =  "birdnotes.rng"
</programlisting>
    </section> <!--SCHEMA_RNG-->
    <section id='YEAR_PAT'>
      <title><code >YEAR_PAT</code >: Year directory name
      pattern</title>
      <para>
        Matches directory names with four digits.
      </para>
      <programlisting role='outFile:birdnotes.py'
>YEAR_PAT = re.compile (
    r'[12]'          # Matches 1 or 2
    r'\d{3}'         # Matches three digits
    r'$' )           # End anchor: insure a complete match
</programlisting>
    </section> <!--YEAR_PAT-->
    <section id='YYYY_MM_XML_PAT'>
      <title><code >YYYY_MM_XML_PAT</code >: Month file name
      pattern</title>
      <para>
        Matches the name of a month file.
      </para>
      <programlisting role='outFile:birdnotes.py'
>YYYY_MM_XML_PAT = re.compile (
    r'[12]'          # Matches 1 or 2
    r'\d{3}'         # Matches three digits
    r'\-'            # Matches a hyphen
    r'[01]'          # Matches 0 or 1
    r'\d'            # Matches any digit
    r'\.xml'         # Matches a period followed by 'xml'
    r'$' )           # End anchor, insure a full match
</programlisting>
    </section> <!--YYYY_MM_XML_PAT-->
  </section> <!--constants-->
  <section id='generic-r-w'>
    <title>Generic node readers and writers: <code
    >.readNode()</code > and <code >.writeNode()</code ></title>
    <para>
      Throughout this module are classes that correspond to nodes
      in the XML document tree.  Each such class will typical
      contain a static <code >.readNode()</code > method that
      converts a DOM tree <code >Element</code > node into a
      class instance, and a regular (non-static) <code
      >.writeNode()</code > method that converts the content of
      an instance back into a DOM <code >Element</code > node.
    </para>
    <para>
      Because all these functions have the same interface, we
      define those interfaces here.  The <code >et</code > module
      is the <filename >lxml.etree</filename > module.
    </para>
    <programlisting
>    .readNode ( node ):   # Static method
      [ node is an et.Element node ->
          if node conforms to &rnc; ->
            return a new class instance that represents that node
          else -> raise IOError ]
    .writeNode ( parent ):
      [ parent is an et.Element instance ->
          parent  :=  parent with a new et.Element added
                      representing self
          return that new et.Element ]
</programlisting>
  </section> <!--generic-r-w-->
  <section id='class-BirdNoteSet'>
    <title><code >class BirdNoteSet</code >: One file's worth of notes</title>
    <para>
      This is the principal exterior interface.  An instance of
      this class is a container for one <code >NOTE_SET_N</code >
      node, the root element of a file conforming to &rnc;.
      See <ulink url='&spec;rnc-note-set.html' >the definition of
      <code >note-set</code > in the schema</ulink >.
    </para>
    <para>
      This object is used in two different contexts:
    </para>
    <itemizedlist>
      <listitem>
        <para>
          It can be used to read an existing notes file.
        </para>
      </listitem>
      <listitem>
        <para>
          It can be used as the back end for a graphical user
          interface application that allows creation of new notes
          files, or editing of existing notes files.
        </para>
      </listitem>
    </itemizedlist>
    <para>
      Because of these differing roles, the class can both read
      its input from XML files, and write its contents back to an
      XML file.  The author prefers to avoid using the features
      of the XML DOM (Document Object Model) that allow in-place
      modification of a document tree.  This puts the burden of
      moving around the tree on the user.  A better solution is
      to use a natural Python data structure, and write it out
      as XML without reference to the logic that can read XML.
    </para>
    <para>
      Here is the class declaration and exported interface:
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   B i r d N o t e S e t

class BirdNoteSet:
    """Represents a NOTE_SET_N element.

      Exports:
        BirdNoteSet ( txny ):
          [ (txny is a taxonomy as a Txny object) and
            (period is the set's period title as a string) ->
              return a new, empty BirdNoteSet using that taxonomy ]
        .txny:          [ as passed to constructor, read-only ]
        .newestTime:
          [ modification epoch time of the most recently
            modified source file read, or None ]
        .period:
          [ the set's period title, initially "", read/write ]
</programlisting>
    <para>
      The constructor requires the <code >txny</code > object so
      that it can translate bird codes into their names and
      taxonomic positions.
    </para>
    <para>
      The <code >.newestTime</code > attribute keeps track
      of the modification time of the most recently modified
      source file read into this instance.  This is used by the
      <code >noteweb</code > application to avoid rebuilding
      XHTML files whose inputs have not changed.
    </para>
    <para>
      The <code >.period</code > attribute is initially empty.
      If the note set is being read from a file, the <code
      >.readFile()</code > method will take care of setting it
      from the input file.  If the note set is being created by a
      GUI, that application will get the period name from the
      user.
    </para>
    <para>
      Here are the exported attributes and methods of the class.
      The <code >.genDays()</code > method is used to retrieve
      the stored records by generating a sequence of <code
      >DayNotes</code > objects.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .genDays():
          [ generate the daily sets in self as DayNotes objects ]
</programlisting>
    <para>
      For file creation through a GUI, the <code >.addDay()</code
      > method appends a new <code >DayNotes</code > object to
      the contents.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .addDay ( dayNotes ):
          [ dayNotes is a DayNotes object ->
              self  :=  self with dayNotes added ]
</programlisting>
    <para>
      The <code >.readFile()</code > method parses an XML file
      and, assuming it conforms to &rnc;, adds the contents of
      that file to the instance.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .readFile ( fileName ):
          [ fileName is a string ->
              if fileName names a readable file that validates
              against &rnc; ->
                self  :=  self with the contents of that file
                          added
              else -> raise IOError ]
</programlisting>
    <para>
      The <code >.writeFile()</code > method creates an entire
      new XML file representing the instance.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .writeFile ( fileName ):
          [ fileName is a string ->
              if fileName names a file that can be created ->
                that file  :=  contents of self
              else -> raise IOError ]
</programlisting>
    <para>
      The instance contains a private attribute <code
      >.__dayList</code > that holds the <code >DayNotes</code >
      elements comprising the content of the instance.
    </para>
    <programlisting role='outFile:birdnotes.py'
>      State/Invariants:
        .__dayList:
          [ a list containing self's daily sets as DayNotes
            objects ]
    """
</programlisting>
    <section id='BirdNoteSet-init'>
      <title><code >BirdNoteSet.__init__()</code >: Constructor</title>
      <para>
        The constructor needs only to store its argument and set
        up the initial state items.  The <code >.__dayList</code
        > is initialized as an empty list.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . _ _ i n i t _ _

    def __init__ ( self, txny ):
        """Constructor for BirdNoteSet
        """
        self.txny        =  txny
        self.period      =  ""
        self.newestTime  =  None
        self.__dayList   =  []
</programlisting>
    </section> <!--BirdNoteSet-init-->
    <section id='BirdNoteSet-addDay'>
      <title><code >BirdNoteSet.addDay()</code >: Add one day list</title>
      <para>
        This method adds a <code >DayNotes</code > object to
        <code >self.__dayList</code >.  There is no requirement
        that the new day be in chronological order relative to
        other contained days.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . a d d D a y

    def addDay ( self, dayNotes ):
        """Add a new day's notes to self.
        """
        self.__dayList.append ( dayNotes )
</programlisting>
    </section> <!--BirdNoteSet-addDay-->
    <section id='BirdNoteSet-genDays'>
      <title><code >BirdNoteSet.genDays()</code >: Generate days
      in self</title>
      <para>
        This method generates the elements of <code
        >self.__dayList</code > without changing their order.
        (If they were put in out of chronological order, that's
        how they will come out.)
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . g e n D a y s

    def genDays ( self ):
        """Generate the DayNotes elements of self.
        """
        for  day in self.__dayList:
            yield day

        raise StopIteration
</programlisting>
    </section> <!--BirdNoteSet-genDays-->
    <section id='BirdNoteSet-readFile'>
      <title><code >BirdNoteSet.readFile()</code >: Add a file's
      content</title>
      <para>
        Use this method to convert an XML file conforming
        to &rnc; into a <code >BirdNoteSet</code > object.
        For the relevant part of the schema, see
        <ulink url='&spec;rnc-note-set.html' >the specification</ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . r e a d F i l e

    def readFile ( self, fileName ):
        """Read content from an XML file.
        """
</programlisting>
      <para>
        We need to keep track of the most recently modified input
        file read; see <xref linkend='BirdNoteSet-fileTime' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if fileName does not exist ->
        #     raise IOError
        #   else if (self.newestTime is None) or
        #   (self.newestTime &lt; modification time of fileName) ->
        #     self.newestTime  :=  modification time of fileName
        #   else -> I ]
        self.__fileTime ( fileName )
</programlisting>
      <para>
        For the logic that reads and validates the XML file, see
        <xref linkend='BirdNoteSet-validate' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if fileName names a readable, well-formed XML file
        #   that validates against SCHEMA_RNG ->
        #     noteSet  :=  the root element of that tree
        #   else ->
        #     raise IOError ]
        noteSet  =  self.__validate ( fileName )
</programlisting>
      <para>
        The root node's <code >period</code > attribute is
        required.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if noteSet has an rnc.PERIOD_A attribute ->
        #     self.period  :=  that attribute
        #   else -> raise IOError ]
        try:
            self.period  =  noteSet.attrib[rnc.PERIOD_A]
        except KeyError:
            raise IOError, ( "The %s element must have a %s "
                "attribute" % 
                (rnc.NOTE_SET_N, rnc.PERIOD_A) )
</programlisting>
      <para>
        All that remains is to process all the <code
        >DAY_NOTES_N</code > elements.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        # [ dayList  :=  list of DAY_NOTES_N children of noteSet ]
        dayList  =  noteSet.xpath ( rnc.DAY_NOTES_N )
</programlisting>
      <para>
        See <xref linkend='BirdNoteSet-readDayNotes' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ self  :=  self with content added from all nodes
        #             in dayList ]
        for  node in dayList:
            self.__readDayNotes ( node )
</programlisting>
    </section> <!--BirdNoteSet-readFile-->
    <section id='BirdNoteSet-fileTime'>
      <title><code >BirdNoteSet.__fileTime()</code >: Update the
      most recent modification time</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . _ _ f i l e T i m e

    def __fileTime ( self, fileName ):
        """Update self.__newestTime

          [ fileName is a string ->
              if fileName does not exist ->
                raise IOError
              else if (self.newestTime is None) or
              (self.newestTime &lt; modification time of fileName) ->
                self.newestTime  :=  modification time of fileName
              else -> I ]
        """
        #-- 1 --
        # [ if fileName names a file that does not exist or is
        #   unreadable ->
        #     raise IOError
        #   else ->
        #     modTime  :=  modification timestamp of that file ]
        if  os.path.exists ( fileName ):
            status  =  os.stat ( fileName )
            modTime  =  status[stat.ST_MTIME]
        else:
            raise IOError, "No such file: '%s'" % fileName

        #-- 2 --
        if  ( (self.newestTime is None) or
              (self.newestTime &lt; modTime) ):
            self.newestTime  =  modTime
</programlisting>
    </section> <!--BirdNoteSet-fileTime-->
    <section id='BirdNoteSet-validate'>
      <title><code >BirdNoteSet.__validate()</code >: Open and
      validate the file</title>
      <para>
        This method takes care of converting the external XML
        file into an <code >et.ElementTree</code > instance.  It
        also validates that tree against the schema.  For details
        of the validation process, see <ulink
        url='http://www.nmt.edu/tcc/help/pubs/pylxml/validation.html'
        ><citetitle >Automated validation of input
        files</citetitle ></ulink > in <ulink
        url='http://www.nmt.edu/tcc/help/pubs/pylxml/'
        ><citetitle >Python XML processing with lxml</citetitle
        ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - - B i r d N o t e S e t . _ _ v a l i d a t e

    def __validate ( self, fileName ):
        """Build an XML tree and validate it against the schema.

          [ fileName is a string ->
              if (SCHEMA_RNG names a readable, well-formed RNG
              bird notes schema) and
              (fileName names a readable XML bird notes file
              that validates against that schema) ->
                return the root node of a document representing
                that bird notes file as an et.Element ]
        """
</programlisting>
      <para>
        Before we can validate the notes file, we have to
        translate the schema itself into an <code
        >ElementTree</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if SCHEMA_RNG names a readable, well-formed XML file ->
        #     schemaDoc  :=  a new et.ElementTree representing
        #                    that file
        #   else -> raise IOError ]
        try:
            schemaDoc  =  et.parse ( SCHEMA_RNG )
        except et.XMLSyntaxError:
            raise IOError, ( "Schema file '%s' is not "
                "well-formed XML." % SCHEMA_RNG )
        except IOError, detail:
            raise IOError, ( "Can't read schema file '%s': %s" %
                (SCHEMA_RNG, str(detail)) )
</programlisting>
      <para>
        The next step is to convert the schema's tree into an
        <code >et.RelaxNG</code > instance that knows how to
        validate against that schema.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if schemaDoc is a valid Relax NG schema ->
        #     schema  :=  an et.RelaxNG instance representing schemaDoc
        #   else -> raise IOError ]
        try:
            schema  =  et.RelaxNG ( schemaDoc )
        except et.RelaxNGParseError, detail:
            raise IOError, ( "File '%s' is not a valid "
                "RNG schema: %s " % (SCHEMA_RNG, str(detail)) )
</programlisting>
      <para>
        Next we convert the bird notes file into a tree.  The
        <code >et.parse()</code > function reads the document and
        turns it into an <code >et.ElementTree</code >.  To find
        the root element of an <code >ElementTree</code >, use
        the <code >.getroot()</code > method.
      </para>
      <para>
        If the file doesn't exist or is unreadable, we'll get an
        <code >IOError</code > exception. If it exists but is not
        well-formed, we get an <code >et.XMLSyntaxError</code >
        exception.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if fileName names a readable, well-formed XML file ->
        #     doc  :=  that file as an et.ElementTree
        #   else -> raise IOError ]
        try:
            doc  =  et.parse ( fileName )
        except et.XMLSyntaxError:
            raise IOError, ( "File '%s' is not well-formed XML." %
                             fileName )
        except IOError, detail:
            raise IOError, ( "Can't read file '%s': %s" %
                (fileName, str(detail)) )
</programlisting>
      <para>
        The <code >schema.validate()</code > method returns 1 if
        a document validates, 0 otherwise.  Assuming all of that
        succeeds, we can then return the document's root element.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        # [ if doc fails to validate against schema ->
        #     raise IOError
        #   else -> I ]
        if  not schema.validate ( doc ):
            raise IOError, ( "File %s is not a valid bird notes "
                "file: %s" % (fileName, schema.error_log) )

        #-- 5 --
        # [ return the root element of doc ]
        return doc.getroot()
</programlisting>
    </section> <!--BirdNoteSet-validate-->
    <section id='BirdNoteSet-readDayNotes'>
      <title><code >BirdNoteSet.__readDayNotes()</code >: Read
      one <code >day-notes</code > node</title>
      <para>
        The <code >node</code > argument is a <code
        >day-notes</code > node as an <code >Element</code >
        node.  This method processes the content of that subtree,
        converting it to a <code >DayNotes</code > object, and
        adding it to <code >self</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . _ _ r e a d D a y N o t e s

    def __readDayNotes ( self, dayNode ):
        """Convert a DAY_NOTES_N node to a DayNotes object.

          [ dayNode is a DAY_NOTES_N node as a DOM Element ->
              if dayNode is valid ->
                self.__dayList  +:=  a new DayNotes element made
                                     from dayNode
              else -> raise IOError ]
        """
</programlisting>
      <para>
        To convert the node into a <code >DayNotes</code > object, we
        use the static method <xref linkend='DayNotes-readNode' />.  It
        may raise <code >IOError</code > if there are any validity
        problems anywhere in the node's subtree.  If everything goes
        well, we add the new instance to <code >self.__dayList</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if dayNode is a valid DAY_NOTES_N node ->
        #     dayNotes  :=  a DayNotes object representing dayNode
        #   else -> raise IOError ]
        dayNotes  =  DayNotes.readNode ( self, self.txny, dayNode )

        #-- 2 --
        self.addDay ( dayNotes )
</programlisting>
    </section> <!--BirdNoteSet-readDayNotes-->
    <section id='BirdNoteSet-writeFile'>
      <title><code >BirdNoteSet.writeFile()</code >: Translate to
      XML</title>
      <para>
        To create an XML representation of a <code
        >BirdNoteSet</code >, we rebuild the XML tree.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . w r i t e F i l e

    def writeFile ( self, fileName ):
        """Translate back to XML.
        """
</programlisting>
      <para>
        First we create the root element, and then install that
        element as the root of an <code >ElementTree</code >
        instance that will contain the whole document.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ tree  :=  an et.ElementTree instance with root element
        #             rnc.NOTE_SET_N
        #   root  :=  that root element as an et.Element instance, with
        #             its rnc.PERIOD_A attribute set to self.period ]
        root  =  et.Element ( rnc.NOTE_SET_N )
        root.attrib[rnc.PERIOD_A]  =  self.period
        tree  =  et.ElementTree ( root )
</programlisting>
      <para>
        Each of the <code >DayNotes</code > instances in this <code
        >note-set</code > will take care of adding the XML for itself
        and its subtree using its <code >.writeNode()</code > method.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ root  :=  root with content added for all DayNote
        #             instances in self ]
        for  dayNotes in self.genDays():
            dayNotes.writeNode ( root )
</programlisting>
      <para>
        Finally, the <code >ElementTree.write()</code > method takes
        care of serializing itself into XML and writing that to the
        file of the given name.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if fileName names a file that can be created new ->
        #     that file  :=  tree as XML
        #   else -> raise IOError ]
        tree.write ( fileName, pretty_print=True )
</programlisting>
    </section> <!--BirdNoteSet-writeFile-->
  </section> <!--class-BirdNoteSet-->
  <section id='class-DayNotes'>
    <title><code >class DayNotes</code >: Notes from one date and state</title>
    <para>
      An object of this class represents the <code
      >day-notes</code > element, containing all the records for
      one day and within one state.  Here is the interface:
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   D a y N o t e s   - - - - -

class DayNotes:
    """Represents one day's notes within one state.

      Exports:
        DayNotes ( noteSet, regionCode, date, daySummary, dayLoc ):
          [ (noteSet is the containing BirdNoteSet object) and
            (regionCode is the enclosing region as a case-insensitive
             US postal state code or the foreign equivalent, e.g.,
             "nm") and
            (date is the fieldwork date as "YYYY-MM-DD") and
            (daySummary is a summary of the field day as
            a DaySummary) and
            (dayLoc is the default location as a Loc instance) ->
                return a new DayNotes object representing those values ]
        .noteSet:           [ as passed to constructor, read-only ]
        .regionCode:        [ as passed to constructor, read-only ]
        .date:              [ as passed to constructor, read-only ]
        .daySummary:        [ as passed to constructor, read-only ]
        .dayLoc:            [ as passed to constructor, read-only ]
        .title():   [ return date+region+dayLoc.name ]
        .defaultLoc():
          [ return the day's default location as a Loc instance ]
        .lookupLoc ( locCode ):
          [ locCode is a location code as a string ->
              if locCode is defined in self (case-sensitive) ->
                return that location as a Loc instance
              else -> raise KeyError ]
        .addForm ( birdForm):
          [ birdForm is a set of one or more sightings of the same
            kind of bird as a BirdForm object ->
              self  :=  self with birdForm added ]
        .genForms(): 
          [ generate the bird records in self in phylogenetic
            order, with records assigned to the same taxon
            sorted by English name ]
        .genFormsSeq():
          [ generate the bird records in self in the order they
            were added ]
        .writeNode ( parent ):
          [ parent is an et.Element instance ->
              parent  :=  parent with a new et.Element node added
                          representing self
              return that new et.Element ]
        DayNotes.readNode ( noteSet, txny, dayNode ):
          [ (noteSet is a BirdNoteSet instance) and
            (txny is a Txny object) and
            (dayNode is a DAY_NOTES_N et.Element) ->
              if all the taxa under dayNode are defined in txny ->
                return a new DayNotes object made from dayNode
              else -> raise IOError ]
</programlisting>
    <para>
      Here is the internal state of this class.  We want to be
      able to access the contained <code >BirdForm</code >
      objects in either of two orders:
    </para>
    <itemizedlist>
      <listitem>
        <para>
          In the order the records were added.  When transcribing
          notes written in the field, we want to preserve the
          order of those records so that we can rearrange the
          records into that order for proofreading.
        </para>
      </listitem>
      <listitem>
        <para>
          In taxonomic order.  The taxonomic key provided in the
          <code >Txny</code > object will sort records into rough
          taxonomic order.  However, in some cases there maybe
          different bird codes that are placed under the same
          taxonomic key.  For example, the hybrids
          Mallard&#x00d7;Pintail and Mallard&#x00d7;Northern
          Shoveler will both be placed in genus <foreignphrase
          >Anas</foreignphrase >, but we want to alphabetize them
          using their English names.
        </para>
        <para>
          This is a good place to demonstrate Python's ability to
          use a tuple as a dictionary key.  We'll use a
          two-element tuple, where the first element is the
          taxonomic key, and the second element is the complete
          English name, e.g., &#x201c;Shoveler, Northern&#x201d;.
        </para>
      </listitem>
    </itemizedlist>
    <programlisting role='outFile:birdnotes.py'>
      State/Invariants:
        .__numberAdded:
          [ the number of BirdForm objects that have ever been
            added to self ]
        .__seqMap:
          [ a dictionary whose keys are integers defining the
            order in which BirdForm objects were added, and
            each corresponding value is that BirdForm object ]
        .__txMap:
          [ a dictionary whose keys are tuples (txKey, name)
            where txKey is the form's taxonomic key in
            self.noteSet.txny and name is the full English name,
            and each corresponding value is the BirdForm object
            for that name ]
    """
</programlisting>
    <section id='DayNotes-title'>
      <title><code >DayNotes.title()</code >: Daily full
      title</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . t i t l e

    def title ( self ):
        """Return the full daily title.
        """
        return ( "%s: %s: %s" %
                 (self.date, self.regionCode.upper(),
                  self.dayLoc.name) )
</programlisting>
    </section> <!--DayNotes-title-->
    <section id='DayNotes-defaultLoc'>
      <title><code >DayNotes.defaultLoc()</code >: Return the
      default location</title>
      <para>
        This method returns a <code >Loc</code > instance for the
        day's sightings that do not provide an explicit location.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . d e f a u l t L o c

    def defaultLoc ( self ):
        """Return self's default location.
        """
        return self.daySummary.defaultLoc()
</programlisting>
    </section> <!--DayNotes-defaultLoc-->
    <section id='DayNotes-lookupLoc'>
      <title><code >DayNotes.lookupLoc()</code >: Look up a location
      code</title>
      <para>
        This method passes the request through to the method of the same
        name in the <code >DaySummary</code > class; see <xref
        linkend='DaySummary-lookupLoc' />.  You might ask, why can't a
        user of this class just use the <code >DayNotes.daySummary</code
        > attribute's <code >.lookupLoc()</code > method?  We can't
        because that would violate the <ulink
        url='http://en.wikipedia.org/wiki/Law_of_Demeter' >Law of
        Demeter</ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . l o o k u p L o c

    def lookupLoc ( self, locCode ):
        """Lookup a location code, return a Loc instance.
        """
        return  self.daySummary.lookupLoc ( locCode )
</programlisting>
    </section> <!--DayNotes-lookupLoc-->
    <section id='DayNotes-addForm'>
      <title><code >DayNotes.addForm()</code >: Add a form to one
      day's notes</title>
      <para>
        This method adds one <code >BirdForm</code > object to
        a <code >DayNotes</code > object.  Each such object added
        over the life of the instance is indexed in the <code
        >self.__seqMap</code > directory with a unique serial
        number; <code >self.__numberAdded</code > keeps track of
        the number of entries added so far.
      </para>
      <para>
        We also add the new <code >BirdForm</code > object to the
        <code >self.__txMap</code > dictionary, with its proper
        two-part key.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . a d d F o r m

    def addForm ( self, newForm ):
        """Add a new BirdForm object to self.
        """
</programlisting>
      <para>
        First we increment <code >self.__numberAdded</code > to
        account for the newly added form object, and remember the
        sequence number (counting from one) in the local variable
        <code >seqNo</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ seqNo  :=  self.__numberAdded + 1
        #   self.__numberAdded  +:=  1 ]
        self.__numberAdded  +=  1
        seqNo  =  self.__numberAdded
</programlisting>
      <para>
        Next, we build the two-tuple used as a key in <code
        >self.__txMap</code >: the first part is the record's
        taxonomic key, and the second part is its English name.
      </para>
    <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ phyloKey  :=  (taxonomic key of newForm,
        #       English name of newForm) ]
        phyloKey  =  ( newForm.birdId.taxon.txKey,
                       str(newForm.birdId) )
</programlisting>
      <para>
        Then we add the new form object to the two internal
        dictionaries.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        self.__seqMap[seqNo]  =  self.__txMap[phyloKey]  =  newForm
</programlisting>
    </section> <!--DayNotes-addForm-->
    <section id='DayNotes-genForms'>
      <title><code >DayNotes.genForms()</code >: Generate forms
      in phylogenetic order</title>
      <para>
        This Python generator returns the sequence of <code
        >BirdForm</code > objects in order by the two-part
        phylogenetic key used in the <code >self.__txMap</code >
        dictionary.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . g e n F o r m s

    def genForms ( self ):
        """Generate contained forms in phylogenetic order.
        """

        #-- 1 --
        # [ keyList  :=  keys from self.txMap in ascending order ]
        keyList  =  self.__txMap.keys()
        keyList.sort()

        #-- 2 --
        # [ generate the values from self.__txMap in order by the
        #   elements of keyList ]
        for  key in keyList:
            yield  self.__txMap[key]

        #-- 3 --
        raise StopIteration
</programlisting>
    </section> <!--DayNotes-genForms-->
    <section id='DayNotes-genFormsSeq'>
      <title><code >DayNotes.genFormsSeq()</code >: Generate
      forms in the order they were added</title>
      <para>
        This method generates the contained <code >BirdForm</code
        > objects in order by their key in <code
        >self.__seqMap</code >, which orders them in the same
        order they were added.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . g e n F o r m s S e q

    def genFormsSeq ( self ):
        """Generate contained forms in phylogenetic order.
        """

        #-- 1 --
        # [ keyList  :=  keys from self.txMap in ascending order ]
        keyList  =  self.__seqMap.keys()
        keyList.sort()

        #-- 2 --
        # [ generate the values from self.__txMap in order by the
        #   elements of keyList ]
        for  key in keyList:
            yield  self.__seqMap[key]

        #-- 3 --
        raise StopIteration
</programlisting>
    </section> <!--DayNotes-genFormsSeq-->
    <section id='DayNotes-init'>
      <title><code >DayNotes.__init__()</code >: Constructor</title>
      <para>
        All we do here is to store the various initial
        attributes, and set up the initial invariants.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . _ _ i n i t _ _

    def __init__ ( self, noteSet, regionCode, date, daySummary,
                   dayLoc=None ):
        #-- 1 --
        # [ self  :=  self with all initial invariants established ]
        self.noteSet  =  noteSet
        self.regionCode  =  regionCode
        self.date  =  date
        self.daySummary  =  daySummary
        self.dayLoc  =  dayLoc
        self.__numberAdded  =  0
        self.__seqMap  =  {}
        self.__txMap  =  {}
</programlisting>
    </section> <!--DayNotes-init-->
    <section id='DayNotes-writeNode'>
      <title><code >DayNotes.writeNode()</code >: Internal form
      to XML</title>
      <para>
        This method generates the XML subtree corresponding to a
        <code >day-notes</code > element.  The <code
        >parent</code > argument is the element under which this
        tree will be a sub-element.  See <ulink
        url='&spec;rnc-day-notes.html' >the definition of <code
        >day-notes</code > in the schema</ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . w r i t e N o d e

    def writeNode ( self, parent ):
        """Generate the XML for a day-notes.
        """
</programlisting>
      <para>
        First we build a dictionary of the attributes that will be
        attached to the new node.  For some of the attributes such as
        <code >date</code >, we could supply the attribute value by
        passing a keyword argument like <code >date="1978-09-20"</code
        > to the <code >et.SubElement()</code > constructor.  However,
        this won't work for attribute names that have hyphens in them:
        Python is not going to like an argument of the form <code
        >day-loc='Soc'</code >.  So, we pre-build the dictionary of
        attribute names and values, and pass it in using Python's
        convention that an argument preceded by &#x201c;<code
        >**</code >&#x201d; is treated as a set of keyword arguments.
      </para>
      <para>
        The <code >day-loc</code > attribute is optional, and is
        needed only when it differs from the default location code in
        the <code >DaySummary</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ attributes  :=  a dictionary mapping rnc.STATE_A to
        #       self.regionCode, rnc.DATE_A to self.date, and
        #       rnc.DAY_LOC_A to self.dayLoc or to None if
        #       self.dayLoc matches self.daySummary.defaultLoc ]
        attributes  =  {
            rnc.STATE_A: self.regionCode,
            rnc.DATE_A: self.date }
        if  self.dayLoc.code != self.daySummary.defaultLoc().code:
            attributes[rnc.DAY_LOC_A]  =  self.dayLoc.code
</programlisting>
      <para>
        Next we create the <code >day-notes</code > element as a
        subelement of <code >parent</code >, using the set of
        attributes we just built.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ parent  :=  parent with a new rnc.DAY_NOTES_N node added
        #       with attributes=(attributes)
        #   selfNode  :=  that node ]
        selfNode  =  et.SubElement ( parent, rnc.DAY_NOTES_N,
            attributes )
</programlisting>
      <para>
        That takes care of all the XML at this level.  Next we attach
        the new node's sub-elements: a <code >day-summary</code >
        element.  See <xref
        linkend='DaySummary-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ selfNode  :=  selfNode with a new rnc.DAY_SUMMARY_N
        #       child appended representing self.daySummary ]
        self.daySummary.writeNode ( selfNode )
</programlisting>
      <para>
        Next we attach the <code >form</code > elements in their
        original order (that is, in order by the keys of <code
        >self.__seqMap</code >).  See <xref linkend='BirdForm-writeNode'
        />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        # [ selfNode  :=  selfNode with new rnc.FORM_N nodes added
        #       representing the values from self.__seqMap in order
        #       according to the keys from self.__seqMap ]
        sequenceKeyList  =  self.__seqMap.keys()
        sequenceKeyList.sort()
        for  sequenceKey in sequenceKeyList:
            #-- 4 body --
            # [ sequenceKey is a key in self.__seqMap ->
            #     selfNode  :=  selfNode with a new rnc.FORM_N node
            #         added representing self.__seqMap[sequenceKey] ]
            birdForm  =  self.__seqMap[sequenceKey]
            birdForm.writeNode ( selfNode )
</programlisting>
    </section> <!--DayNotes-writeNode-->
    <section id='DayNotes-readNode'>
      <title><code >DayNotes.readNode()</code >: XML to internal form</title>
      <para>
        This method extracts from the <code >date</code > element
        all the information required to build a <code
        >DayNotes</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . r e a d N o d e    (static method)

    def readNode ( noteSet, txny, dayNode ):
        """Convert an rnc.DAY_NOTES_N node to a DayNotes instance.
        """
</programlisting>
      <para>
        The region code and date come directly from <code
        >dayNode</code >.  The day location code is an optional
        <code >day-loc</code > attribute.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ regionCode  :=  rnc.STATE_A attribute from dayNode
        #   date  :=  rnc.DATE_A attribute from dayNode ]
        regionCode  =  dayNode.attrib [ rnc.STATE_A ]
        date  =  dayNode.attrib [ rnc.DATE_A ]

        #-- 2 --
        # [ if dayNode has an rnc.DAY_LOC_A attribute ->
        #     dayLocCode  :=  that attribute
        #   else ->
        #     dayLocCode  :=  None ]
        try:
            dayLocCode  =  dayNode.attrib [ rnc.DAY_LOC_A ]
        except KeyError:
            dayLocCode  =  None
</programlisting>
      <para>
        At this point we process the <code >day-summary</code > element
        using the static method <xref linkend='DaySummary-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if  dayNode has a valid rnc.DAY_SUMMARY_N child
        #   element ->
        #     daySummary  :=  a new DaySummary instance made
        #                     from that child element
        #   else -> raise IOError ]
        summaryNode  =  dayNode.xpath ( rnc.DAY_SUMMARY_N )[0]
        daySummary  =  DaySummary.readNode ( summaryNode )
</programlisting>
      <para>
        Now that we have read the <code >day-summary</code > element, we
        must be sure that the <code >dayLocCode</code > is defined, if
        it was given.  See <xref linkend='DaySummary-defaultLoc' /> and
        <xref linkend='DaySummary-lookupLoc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        # [ if dayLocCode is None ->
        #     dayLoc  :=  default location from daySummary
        #   else if dayLocCode is defined in daySummary ->
        #     dayLoc  :=  dayLocCode's location from daySummary
        #   else ->
        #     raise IOError ]
        if  dayLocCode is None:
            dayLoc  =  daySummary.defaultLoc()
        else:
            try:
                dayLoc  =  daySummary.lookupLoc ( dayLocCode )
            except KeyError:
                raise IOError, ( "%s '%s' is not defined in the "
                    "%s element." %
                    (rnc.DAY_LOC_A, dayLocCode, rnc.DAY_SUMMARY_N) )
</programlisting>
      <para>
        At this point, we have enough information to create the a <code
        >DayNotes</code > instance.  See <xref linkend='DayNotes-init'
        />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ dayNotes  :=  a new DayNotes instance with
        #       noteSet=noteSet, regionCode=regionCode, date=date,
        #       daySummary=daySummary, and dayLoc=dayLoc ]
        dayNotes  =  DayNotes ( noteSet, regionCode, date, daySummary,
                                dayLoc )
</programlisting>
      <para>
        Next we process all the <code >form</code > nodes and add
        them to <code >self</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        # [ if the subtree rooted in dayNode conforms to
        #   birdnotes.rnc ->
        #     self  :=  self with BirdForm objects added representing
        #               all rnc.FORM_N children of dayNode ]
        #   else ->
        #     self  :=  (anything)
        #     raise  IOError ]
        for  formNode in dayNode.getiterator ( rnc.FORM_N ):
            #-- 6 body --
            # [ formNode is an et.Element ->
            #     if formNode roots a valid rnc.FORM_N subtree
            #     in the context of txny ->
            #       self  :=  self with a BirdForm object added
            #                 representing formNode
            #     else -> raise IOError ]
            dayNotes.readForm ( txny, formNode )
</programlisting>
      <para>
        Finally, return the new instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 7 --
        return dayNotes

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--DayNotes-readNode-->
    <section id='DayNotes-readForm'>
      <title><code >DayNotes.readForm()</code >: Process one
      contained form</title>
      <para>
        This method translates a <code >form</code > element into
        a <code >BirdForm</code > instance, and adds it to <code
        >self</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y N o t e s . r e a d F o r m

    def readForm ( self, txny, formNode ):
        """Translate a form node to a FormNode instance.

          [ (txny is a Txny instance) and
            (formNode is an et.Element) ->
              if formNode roots a valid rnc.FORM_N subtree
              in the context of txny ->
                self  :=  self with a BirdForm object added
                          representing formNode
              else -> raise IOError ]            
        """
</programlisting>
      <para>
        First we call the static method <xref
        linkend='BirdForm-readNode' /> to convert the <code
        >formNode</code > into a <code >BirdForm</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if formNode is a valid rnc.FORM_N element ->
        #     birdForm  :=  a BirdForm instance representing that
        #                   element
        #   else -> raise IOError ]
        birdForm  =  BirdForm.readNode ( txny, self, formNode )
</programlisting>
      <para>
        Then, if all goes well, we add it to <code >self</code >.
        See <xref linkend='DayNotes-addForm' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ self  :=  self with birdForm added ]
        self.addForm ( birdForm )
</programlisting>
    </section> <!--DayNotes-readForm-->
  </section> <!--class-DayNotes-->
  <section id='class-DaySummary'>
    <title><code >class DaySummary</code>: Daily summary</title>
    <para>
      An instance of this class represents a <code
      >day-summary</code > element.  See <ulink
      url='&spec;rnc-day-summary.html' >the definition of the <code
      >day-summary</code > pattern in the schema</ulink >.
    </para>
    <para>
      The only item required by the constructor is the
      default location code.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   D a y S u m m a r y

class DaySummary:
    """Represents a day-summary element.

      Exports:
        DaySummary ( defaultLocCode ):
          [ defaultLocCode is a location code as a string ->
              return a new DaySummary instance with that
              default location code ]
        .defaultLocCode:  [ as passed to constructor ]
</programlisting>
    <para>
      Locations are added using the <code >.addLoc()</code >
      method.  The other attributes&#x2014;route, film and
      such&#x2014;are added by storing <code >Narrative</code >
      instances directly into attributes of the method.  This,
      in the author's opinion, does not break encapsulation
      because the caller must satisfy the invariant on the
      attribute.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .defaultLoc():
          [ return the default location as a Loc instance ]
        .addLoc ( loc ):
          [ loc is a location as a Loc instance ->
              if self has no location with the same code ->
                self  :=  self with that location added
              else -> raise KeyError ]
        .lookupLoc ( locCode ):
          [ locCode is a location code as a string ->
              if locCode is defined in self (case-sensitive) ->
                return that location as a Loc instance
              else -> raise KeyError ]
        .genLocs():
          [ generate the locations in self as a sequence of
            Loc instances in ascending order by code ]
        .route:
          [ if self has a route description ->
              that description as a Narrative instance
            else -> None ]
        .weather:
          [ if self has a weather description ->
              that description as a Narrative instance
            else -> None ]
        .missed:
          [ if self has a missed-species description ->
              that description as a Narrative instance
            else -> None ]
        .film:
          [ if self has a film description ->
              that description as a Narrative instance
            else -> None ]
        .notes:
          [ if self has general notes ->
              those notes as a Narrative instance
            else -> None ]
</programlisting>
      <para>
        The class has the usual methods for reading and writing
        XML.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        DaySummary.readNode ( node ):  # Static method
          [ node is an et.Element ->
              if node conforms to birdnotes.rnc ->
                return a new DaySummaryclass instance that
                represents that node
              else -> raise IOError ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with a new et.Element added
                          representing self
              return that new et.Element ]       
</programlisting>
    <para>
      These additional state items are used to manage the content.
    </para>
    <programlisting role='outFile:birdnotes.py'
>      State/Invariants:
        .__locCodeMap:
          [ a dictionary whose values are the locations in self
            as Loc instances, and each the corresponding key is
            the location code ]
    """
</programlisting>
    <section id='DaySummary-init'>
      <title><code >DaySummary.__init__()</code ></title>
      <para>
        Note that the constructor has to store the default
        location code away and hope that someone later adds
        the definition&#x2014;this is actually done by
        <xref linkend='DaySummary-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . _ _ i n i t _ _

    def __init__ ( self, defaultLocCode ):
        """Constructor
        """
        self.defaultLocCode  =  defaultLocCode
        self.route  =  None
        self.weather  =  None
        self.missed  =  None
        self.film  =  None
        self.notes  =  None
        self.__locCodeMap  =  {}
</programlisting>
    </section> <!--DaySummary-init-->
    <section id='DaySummary-defaultLoc'>
      <title><code >DaySummary.defaultLoc()</code >: Return the
      default location</title>
      <para>
        This method returns the <code >Loc</code > instance
        corresponding to the default location code.  See
        <xref linkend='DaySummary-lookupLoc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . d e f a u l t L o c

    def defaultLoc ( self ):
        """Return self's default location.
        """
        #-- 1 --
        # [ if self.defaultLocCode is a valid location code in
        #   self ->
        #     return the corresponding Loc instance
        #   else -> raise KeyError ]
        return  self.lookupLoc ( self.defaultLocCode )
</programlisting>
    </section> <!--DaySummary-defaultLoc-->
    <section id='DaySummary-addLoc'>
      <title><code >DaySummary.addLoc()</code >: Add a location
      definition</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . a d d L o c

    def addLoc ( self, loc ):
        """Add a location.
        """
        if  self.__locCodeMap.has_key ( loc.code ):
            raise KeyError, ( "Duplicate location code '%s'" %
                              loc.code )
        self.__locCodeMap[loc.code]  =  loc
</programlisting>
    </section> <!--DaySummary-addLoc-->
    <section id='DaySummary-lookupLoc'>
      <title><code >DaySummary.lookupLoc()</code >: Find a
      location by its code</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . l o o k u p L o c

    def lookupLoc ( self, locCode ):
        """Lookup a location by its code.
        """
        #-- 1 --
        # [ if self has a location whose code matches locCode ->
        #     return the corresponding location as a Loc instance
        #   else -> raise KeyError ]
        return  self.__locCodeMap [ locCode ]
</programlisting>
    </section> <!--DaySummary-lookupLoc-->
    <section id='DaySummary-genLocs'>
      <title><code >DaySummary.genLocs()</code >: Generate all
      locations</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . g e n L o c s

    def genLocs ( self ):
        """Generate the locations in self.
        """

        #-- 1 --
        # [ codeList  :=  keys of self.__locCodeMap, in ascending
        #                 order ]
        codeList  =  self.__locCodeMap.keys()
        codeList.sort()

        #-- 2 --
        # [ generate values of self.__locCodemap in order by
        #   codeList ]
        for  code in codeList:
            yield  self.__locCodeMap[code]

        #-- 3 --
        raise StopIteration
</programlisting>
    </section> <!--DaySummary-genLocs-->
    <section id='DaySummary-readNode'>
      <title><code >DaySummary.readNode()</code >: Convert from
      XML (static method)</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Convert from XML.
        """
</programlisting>
      <para>
        The <code >default-loc</code > attribute is required, and its
        value is all we need to create a new <code >DaySummary</code >
        instance.  See <xref linkend='DaySummary-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ defaultLocCode  :=  node's rnc.DEFAULT_LOC_A
        #                       attribute ]
        defaultLocCode  =  node.attrib[rnc.DEFAULT_LOC_A]

        #-- 2 --
        # [ daySummary  :=  a new DaySummary instance for
        #       default location code (defaultLocCode) ]
        daySummary  =  DaySummary ( defaultLocCode )
</programlisting>
      <para>
        Direct children of a <code >day-summary</code > element
        must start with one or more <code >loc</code > children,
        followed by the various <code >day-annotation</code >
        elements in no particular order.  We'll use XPath
        expressions to corral and process these children.  First,
        the <code >loc</code > children.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ locNodeList  :=  rnc.LOC_N children of node ]
        locNodeList  =  node.xpath ( rnc.LOC_N )
</programlisting>
      <para>
        See <xref linkend='Loc-readNode' /> and <xref
        linkend='DaySummary-addLoc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        # [ daySummary  :=  daySummary with locations added
        #       from elements of locNodeList ]
        for  locNode in locNodeList:
            #-- 4 body --
            # [ locNode is an rnc.LOC_N node ->
            #       self  :=  self with a location added made
            #                 from locNode ]
            loc  =  Loc.readNode ( locNode )
            daySummary.addLoc ( loc )
</programlisting>
      <para>
        Finally, we look for the various other child nodes.
        See <xref linkend='DaySummary-dayAnnotation' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ daySummary  :=  daySummary with information added from
        #       any day-annotation children of (node) ]
        daySummary.dayAnnotation ( node )
</programlisting>
      <para>
        Now that we have read all the definitions of today's location
        codes, we must perform an important validity check: there must
        be a definition for the default location code.  If that
        succeeds, we're done, and can return the new <code
        >DaySummary</code > instance to the caller.  See <xref
        linkend='DaySummary-lookupLoc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        # [ if defaultLocCode is a valid location code in
        #   self ->
        #     I
        #   else ->
        #     raise IOError ]
        try:
            test  =  daySummary.lookupLoc ( defaultLocCode )
        except KeyError:
            raise IOError, ( "Default location code '%s' is not "
                "defined." % defaultLocCode )

        #-- 7 --
        return  daySummary

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--DaySummary-readNode-->
    <section id='DaySummary-dayAnnotation'>
      <title><code >DaySummary.dayAnnotation()</code >: Process
      <code >day-annotation</code > content</title>
      <para>
        This method is part of the process of translating a <code
        >day-summary</code > node into a <code >DaySummary</code
        > instance.  See <ulink url='&spec;rnc-day-annotation.html'
        >the definition of the <code >day-annotation</code >
        pattern in the schema</ulink >.
      </para>
      <para>
        The <code >node</code > argument is the <code
        >day-summary</code > element.  We look for all the
        element children that are defined in the <code
        >day-annotation</code > pattern of the schema, and add
        attributes to the instance for any that we find.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . d a y A n n o t a t i o n

    def dayAnnotation ( self, node ):
        """Read day-annotation content.

          [ node is an rnc.DAY_SUMMARY node as an et.Element ->
              self  :=  self with information added from any
                        day-annotation children of (node) ]
        """
</programlisting>
      <para>
        First we look for the elements that can occur only
        once: <code >route</code >, <code >weather</code >, <code
        >missed</code >, and <code >film</code >.  Each of these
        elements has <code >narrative</code > content that is processed
        by <xref linkend='Narrative-readChild' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if node has at least one rnc.ROUTE_N child ->
        #     self.route  :=  an Annotation instance representing
        #                     that child's content
        #   else -> I ]
        self.route  =  Narrative.readChild ( node, rnc.ROUTE_N )

        #-- 2 --
        # [ if node has at least one rnc.WEATHER_N child ->
        #     self.weather  :=  an Annotation instance representing
        #                     that child's content
        #   else -> I ]
        self.weather  =  Narrative.readChild ( node, rnc.WEATHER_N )

        #-- 3 --
        # [ if node has at least one rnc.MISSED_N child ->
        #     self.missed  :=  an Annotation instance representing
        #                     that child's content
        #   else -> I ]
        self.missed  =  Narrative.readChild ( node, rnc.MISSED_N )

        #-- 4 --
        # [ if node has at least one rnc.FILM_N child ->
        #     self.film  :=  an Annotation instance representing
        #                     that child's content
        #   else -> I ]
        self.film  =  Narrative.readChild ( node, rnc.FILM_N )
</programlisting>
      <para>
        The <code >para</code > child is different:  there may be
        any number of them.  We'll build a <code >Narrative</code
        > instance, adding each <code >para</code > to it.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ paraNodeList  :=  list of all rnc.PARA_N children
        #                     of node ]
        paraNodeList  =  node.xpath ( rnc.PARA_N )
</programlisting>
      <para>
        See <xref linkend='Narrative-init' /> and <xref
        linkend='Paragraph-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        # [ if paraNodeList is non-empty ->
        #      self.notes  :=  a new Narrative instance with paragraphs
        #          added made from the elements of paraNodeList
        #   else -> I ]
        if  len(paraNodeList) &gt; 0:
            #-- 6.1 --
            # [ self.notes  :=  a new, empty Narrative instance ]
            self.notes  =  Narrative()

            #-- 6.2 --
            # [ narrative  :=  narrative with paragraphs added
            #       made from the elements of paraNodeList ]
            for  paraNode in paraNodeList:
                para  =  Paragraph.readNode ( paraNode )
                self.notes.addPara ( para )
</programlisting>
    </section> <!--DaySummary-dayAnnotation-->
    <section id='DaySummary-writeNode'>
      <title><code >DaySummary.writeNode()</code >: Translate to
      XML</title>
      <para>
        This method attaches a <code >day-summary</code > subtree
        to the given <code >parent</code >, made from <code
        >self</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   D a y S u m m a r y . w r i t e N o d e

    def writeNode ( self, parent ):
        """Translate self to XML.
        """
</programlisting>
      <para>
        First we build the <code >day-summary</code > node
        itself, and attach its <code >default-loc</code >
        attribute.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ parent  :=  parent with a new rnc.DAY_SUMMARY_N node
        #       added with its rnc.DEFAULT_LOC_A attribute set to
        #       self.defaultLocCode
        #   result  :=  that new node ]
        result  =  et.SubElement ( parent, rnc.DAY_SUMMARY_N )
        result.attrib[rnc.DEFAULT_LOC_A]  =  self.defaultLocCode
</programlisting>
      <para>
        Next, add the locations.  See <xref linkend='Loc-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ result  :=  result with rnc.LOC_N nodes added, made
        #       from the locations in self ]
        for  loc in self.genLocs():
            locNode  =  loc.writeNode ( result )
</programlisting>
      <para>
        Finally, add the various <code >day-annotation</code > elements.
        In each case, this addition is done by <xref
        linkend='Narrative-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if  self.route is not None ->
        #     result  :=  result with an rnc.ROUTE_N node added
        #                 containing self.route's narrative
        #   else -> I ]
        if  self.route:
            routeNode  =  et.SubElement ( result, rnc.ROUTE_N )
            self.route.writeNode ( routeNode )

        #-- 4 --
        # [ if  self.weather is not None ->
        #     result  :=  result with an rnc.WEATHER_N node added
        #                 containing self.weather's narrative
        #   else -> I ]
        if  self.weather:
            weatherNode  =  et.SubElement ( result, rnc.WEATHER_N )
            self.weather.writeNode ( weatherNode )

        #-- 5 --
        # [ if  self.missed is not None ->
        #     result  :=  result with an rnc.MISSED_N node added
        #                 containing self.missed's narrative
        #   else -> I ]
        if  self.missed:
            missedNode  =  et.SubElement ( result, rnc.MISSED_N )
            self.missed.writeNode ( missedNode )

        #-- 6 --
        # [ if  self.film is not None ->
        #     result  :=  result with an rnc.FILM_N node added
        #                 containing self.film's narrative
        #   else -> I ]
        if  self.film:
            filmNode  =  et.SubElement ( result, rnc.FILM_N )
            self.film.writeNode ( filmNode )
</programlisting>
      <para>
        Finally, add the <code >notes</code > narrative if
        present.  They will be added as <code >para</code >
        elements directly under the <code >day-summary</code >
        node, unlike the above four where the narrative is added
        under a child node.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 7 --
        # [ if  self.notes is not None ->
        #     result  :=  result with rnc.PARA_N nodes added
        #                 from self.notes
        #   else -> I ]
        if  self.notes:
            self.notes.writeNode ( result )
</programlisting>
    </section> <!--DaySummary-writeNode-->
  </section> <!--class-DaySummary-->
  <section id='class-Loc'>
    <title><code >class Loc</code >: Locality code definition</title>
    <para>
      An instance of this class defines one locality code used
      within one day's notes.  See <ulink url='&spec;rnc-loc.html'
      >the definition of the <code >loc</code > pattern in the
      schema</ulink >.
    </para>
    <para>
      The <code >.text</code > attribute can be passed to the
      constructor, or added later by direct write to the
      instance's attribute.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   L o c

class Loc:
    """Represents one location.

      Exports:
        Loc ( code, name, text=None ):
          [ (code is a locality code as a string) and
            (name is the locality's name as a string) and
            (text is the locality's full description as a string,
            or None) ->
              return a new Loc instance with those values ]
        .code:     [ as passed to constructor, read-only ]
        .name:     [ as passed to constructor, read-only ]
        .text:     [ as passed to constructor, read-write ]
</programlisting>
    <para>
      An instance is also a container for any number of GPS
      waypoints.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .addGps ( gps ):
          [ gps is a waypoint as a Gps instance ->
               self  :=  self with gps added ]
        .genGps():
          [ generate the waypoints in self in the order
            they were added ]         
</programlisting>
      <para>
        The class has the usual <code >.readNode()</code > and
        <code >.writeNode()</code > methods.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        Loc.readNode ( node ):   # Static method
          [ node is an rnc.LOC_N node as an et.Element ->
              return a new Loc instance made from node ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with self's content added
                  as a new rnc.LOC_N child element
              return that new child element ]
</programlisting>
    <para>
      Internal state:
    </para>
    <programlisting role='outFile:birdnotes.py'
>      State/Invariants:
        .__gpsList:
          [ list of waypoints in self in the order they were
            added, as Gps instances ]
    """
</programlisting>
    <section id='Loc-init'>
      <title><code >Loc.__init__()</code >: Constructor</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c . _ _ i n i t _ _

    def __init__ ( self, code, name, text=None ):
        """Constructor for Loc.
        """
        self.code  =  code
        self.name  =  name
        self.text  =  text
        self.__gpsList  =  []
</programlisting>
    </section> <!--Loc-init-->
    <section id='Loc-addGps'>
      <title><code >Loc.addGps()</code >: Add a waypoint</title>
      <para>
        Waypoints are kept in a simple list, in the order they
        are added.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c . a d d G p s

    def addGps ( self, gps ):
        """Add one waypoint.
        """
        self.__gpsList.append ( gps )
</programlisting>
    </section> <!--Loc-addGps-->
    <section id='Loc-genGps'>
      <title><code >Loc.genGps()</code >: Generate waypoints</title>
      <para>
        This method generates the GPS waypoints in a <code >Loc</code >
        instance as a sequence of <code >Gps</code > instances.
        Because these waypoints are stored in sequence in the <code
        >.__gpsList</code > attribute, we need only return an iterator
        that visits the elements of that list, because an iterator is
        also a generate that generates the values in the structure
        over which it is iterating.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c . g e n G p s

    def genGps ( self ):
        """Generate the waypoints in self.
        """
        return  iter(self.__gpsList)
</programlisting>
    </section> <!--Loc-genGps-->
    <section id='Loc-readNode'>
      <title><code >Loc.readNode()</code >: Convert from XML
      (static method)</title>
      <para>
        This method reconstructs the <code >node</code > element
        and its children.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Convert XML to a Loc instance.
        """
</programlisting>
      <para>
        First we'll create a new <code >Loc</code > instance
        with attribute values taken from the <code >node</code >.
        See <xref linkend='Loc-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ code  :=  rnc.CODE_A attribute from node
        #   name  :=  rnc.NAME_A attribute from node ]
        code  =  node.attrib[rnc.CODE_A]
        name  =  node.attrib[rnc.NAME_A]

        #-- 2 --
        # [ loc  :=  a new Loc instance with code=(code) and
        #            name=(name) and no text ]
        loc  =  Loc ( code, name )
</programlisting>
      <para>
        Next we look to see if there are any <code >gps</code >
        children and, if so, make them into <code >Gps</code >
        instances and add them to the <code >Loc</code >
        instance.  See <xref linkend='Gps-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ gpsNodeList  :=  a list containing all rnc.GPS_N
        #                    children of node
        #   lastGps  :=  None ]
        gpsNodeList  =  node.xpath ( rnc.GPS_N )

        #-- 4 --
        # [ if gpsNodeList is non-empty ->
        #     loc  :=  loc with Gps instances added, made from
        #              elements of gpsNodeList
        #     lastGps  :=  the last Gps instance added
        #   else -> I ]
        for  gpsNode in gpsNodeList:
            lastGps  =  Gps.readNode ( gpsNode )
            loc.addGps ( lastGps )
</programlisting>
      <para>
        Next we check for the optional narrative describing the
        location.  To get around the strange way <code
        >lxml</code > handles text, we'll use the XPath
        expression <code >"text()"</code > to get a list of all
        the text node children of <code >node</code >, which we
        will then concatenate.  If any of the <code >gps</code >
        element children have text nodes, they will not be
        included.  The <code >.strip()</code > method will delete
        leading and trailing space, and if there is nothing left,
        we consider that no narrative.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ if  (node has any nonblank text children) and
        #   (lastGps is None) ->
        #     loc.text  :=  all text children, minus leading and
        #                   trailing space
        #   else if node has any nonblank text children ->
        #     lastGps.tail  :=  all text children, minus leading
        #                       and trailing space
        #   else -> I ]
        textList  =  node.xpath ( 'text()' )
        s  =  "".join(textList).strip()
        if  s:
            loc.text  =  s
</programlisting>
      <para>
        All that remains is to return the newly built <code
        >Loc</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        return  loc

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--Loc-readNode-->
    <section id='Loc-writeNode'>
      <title><code >Loc-writeNode()</code >: Translate to XML</title>
      <para>
        This method attaches a <code >loc</code > element to
        the given <code >parent</code > node.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c . w r i t e N o d e

    def writeNode ( self, parent ):
        """Build a loc element and its subtree.
        """
        #-- 1 --
        # [ parent  :=  parent with a new rnc.LOC_N node added
        #   result  :=  that node ]
        result  =  et.SubElement ( parent, rnc.LOC_N )

        #-- 2 --
        # [ result  :=  result with an rnc.CODE_A attribute added
        #       made from self.code and an rnc.NAME_A from self.name ]
        result.attrib [ rnc.CODE_A ]  =  self.code
        result.attrib [ rnc.NAME_A ]  =  self.name
</programlisting>
      <para>
        Next, we'll add child nodes for the waypoints, if any.
        See <xref linkend='Gps-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if  self.__gpsList is empty ->
        #     lastGpsNode  :=  None
        #   else ->
        #     result  :=  result with rnc.GPS_N children added,
        #                 made from self.__gpsList
        #     lastGpsNode  :=  the last such child added ]
        lastGpsNode  =  None
        for  gps in self.__gpsList:
            lastGpsNode  =  gps.writeNode ( result )
</programlisting>
      <para>
        Adding the text is a little tricky because of the way
        <code >lxml</code > handles text.  If there are any <code
        >gps</code > children, <code >lxml</code > wants the text
        to be in the <code >.tail</code > attribute of the last
        child element.  However, there may not be any <code
        >gps</code > children, in which case <code >lastGps</code
        > will be <code >None</code >; in this case, the text
        goes into the <code >.text</code > attribute of the <code
        >loc</code > node.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        if  self.text:
            if  lastGpsNode is None:
                result.text  =  self.text
            else:
                lastGpsNode.tail  =  self.text

        #-- 5 --
        return result
</programlisting>
    </section> <!--Loc-writeNode-->
  </section> <!--class-Loc-->
  <section id='class-Gps'>
    <title><code >class Gps</code >: GPS waypoint</title>
    <para>
      Each instance represents a waypoint recorded by a Global
      Positioning System.  See <ulink url='&spec;rnc-gps.html' >the
      definition of the <code >gps</code > element in the
      schema</ulink >.
    </para>
    <para>
      The string representing the coordinates is required;
      textual description is optional.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   G p s

class Gps:
    """Represents a GPS waypoint.

      Exports:
        Gps ( waypoint, text=None ):
          [ (waypoint is a lat-long as a string) and
            (text is a description as a string, or None) ->
              if waypoint is a valid lat-long string ->
                return a new Gps instance with those values
              else -> raise ValueError ]
        .waypoint:   [ as passed to constructor, read-only ]
        .text:  [ as passed to constructor, read-write ]
        .latLon:
          [ a terrapos.LatLon instance representing self.waypoint ]
        Gps.readNode ( node ):    # Static method
          [ node is an rnc.GPS_N node as an et.Element ->
              if node conforms to &rnc; ->
                return a new Gps instance representing node
              else -> raise IOError ]
        .writeNode ( parent ):
          [ parent is an et.Element instance ->
              parent  :=  parent with a new rnc.GPS_N node added
                          representing self
              return that new node ]
    """
</programlisting>
    <section id='Gps-init'>
      <title><code >Gps.__init__()</code ></title>
      <para>
        In addition to the usual work of storing copies of the
        constructor's arguments, we'll use the <code
        >terrapos</code > module to valid the structure of the
        waypoint string, and store the equivalent <code
        >LatLon</code > instance as well.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   G p s . _ _ i n i t _ _

    def __init__ ( self, waypoint, text=None ):
        """Constructor for Gps
        """
        #-- 1 --
        self.waypoint  =  waypoint
        self.text  =  text

        #-- 2 --
        # [ if waypoint is a valid lat-lon as a string ->
        #     self.latLon  :=  a terrapos.LatLon instance
        #                      representing waypoint
        #   else -> raise ValueError ]
        self.latLon  =  terrapos.scanLatLon ( waypoint )
</programlisting>
    </section> <!--Gps-init-->
    <section id='Gps-readNode'>
      <title><code >Gps.readNode()</code > (static method)</title>
      <para>
        This method constructs a <code >Gps</code > instance from
        its XML representation.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   G p s . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Convert from XML
        """
        waypoint  =  node.attrib [ rnc.WAYPOINT_A ]
        text  =  node.text
        return  Gps ( waypoint, text )

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--Gps-readNode-->
    <section id='Gps-writeNode'>
      <title><code >Gps.writeNode()</code >: Convert to XML</title>
      <para>
        This method converts a <code >Gps</code > instance into a
        <code >gps</code > XML element.  The <code
        >waypoint</code > attribute is required.  Text will be
        added to the element only if it is not <code >None</code
        > in the instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   G p s . w r i t e N o d e

    def writeNode ( self, parent ):
        """Convert to XML
        """
        #-- 1 --
        # [ parent  :=  parent with a new rnc.GPS_N child added
        #       with rnc.WAYPOINT_A attribute set to self.waypoint ]
        gpsNode  =  et.SubElement ( parent, rnc.GPS_N )
        gpsNode.attrib [ rnc.WAYPOINT_A ]  =  self.waypoint

        #-- 2 --
        # [ if self.text is not None ->
        #     gpsNode  :=  gpsNode with its content set to self.text
        #   else -> I ]
        if  self.text is not None:
            gpsNode.text  =  self.text

        #-- 3 --
        return gpsNode
</programlisting>
    </section> <!--Gps-writeNode-->
  </section> <!--class-Gps-->
  <section id='class-BirdForm'>
    <title><code >class BirdForm</code >: Notes for one kind of bird</title>
    <para>
      An instance of this class represents the XML <code
      >form</code > element.  See <ulink url='&spec;rnc-form.html'
      >the definition of the <code >form</code > element in the
      schema</ulink >.
    </para>
    <para>
      In much of this program, the structure of objects follows
      the XML structure fairly closely.  For example, where the
      XML has a <code >note-set</code > root element with <code
      >day-notes</code > children, the object structure has a
      <code >BirdNoteSet</code > root with <code >DayNotes</code
      > children.  However, in the <code >BirdForm</code > class,
      we deviate from that structure.  A <code >Sighting</code
      > instance is similar but not identical to a <code
      >floc</code > element in the XML.
    </para>
    <para>
      In the XML, the <code >floc</code > element is optional,
      and used only when multiple sightings of the same kind of
      bird differ in their ages, sexes, locality codes, or
      other details.  It would have been more consistent to
      design the schema such that the <code >form</code >
      element carried the species code, and the rest had to be
      wrapped in a <code >floc</code > element that would carry
      age, sex, and other data.  However, since the vast majority
      of records have only one <code >floc</code > element, and
      since the initial XML file creation was done by hand (using
      <application >emacs</application >), the schema's <code
      >single-sighting</code > pattern allows the <code
      >floc</code > level to be omitted.
    </para>
    <para>
      In any case, each <code >BirdForm</code > instance has one
      or more <code >Sighting</code > instance children.  An XML
      <code >form</code > element with no <code >floc</code >
      children will be translated into a <code >BirdForm</code >
      instance with one <code >Sighting</code > child, while an
      XML <code >form</code > element with three <code
      >floc</code > children will be translated into a <code
      >BirdForm</code > instance with three <code >Sighting</code
      > children.
    </para>
    <para>
      Note that the schema allows both <code >form</code > and <code
      >floc</code > elements to carry <code >loc-group</code > and <code
      >sighting-notes</code > content.  The internal representation
      depends on whether this is the single-sighting or multi-sighting
      case.
    </para>
    <itemizedlist>
      <listitem>
        <para>
          For the single-sighting case, any <code >age-sex-group</code
          >, <code >loc-group</code > or
          <code >sighting-notes</code > content from to the <code
          >form</code > element is attached to the single <code
          >Sighting</code > child instance.
        </para>
      </listitem>
      <listitem>
        <para>
          For the multi-sighting case, the <code >form</code > element
          cannot contain any <code >age-sex-group</code > content, but
          any <code >loc-group</code > or <code >sighting-notes</code >
          content from the <code >form</code > element is attached to
          the <code >BirdForm</code > instance.  Each child <code
          >floc</code > element is made into a child <code
          >Sighting</code > instance that carries any <code
          >age-sex-group</code >, <code >loc-group</code > or <code
          >sighting-notes</code > content from that element.
        </para>
      </listitem>
    </itemizedlist>
    <para>
      For more discussion about potential differences between XML
      as read and XML as written, see <code
      >BirdForm-writeNode</code >.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   B i r d F o r m

class BirdForm:
    """Represents one or more sightings of a single kind of bird.

      Exports:
        BirdForm ( txny, dayNotes, ab6, rel=None, alt=None,
                   notable=None ):
          [ (txny is a taxonomy as a txny.Txny instance) and
            (dayNotes is a DayNotes instance) and
            (ab6 is a six-letter bird code as a string) and
            (rel is a relationship code or None) and
            (alt is a six-letter bird code or None) and
            (notable is true iff the sighting is notable) ->
              return a new BirdForm instance with those values,              
              containing no sightings ]
        .dayNotes:    [ as passed to constructor, read-only ]
        .ab6:         [ as passed to constructor, read-only ]
        .rel:         [ as passed to constructor, read-only ]
        .alt:         [ as passed to constructor, read-only ]
        .notable:     [ as passed to constructor, read-only ]
</programlisting>
    <para>
      We don't accept just any old bird codes; they have to be
      validated against a <code >txny.Txny</code > instance.
      The <code >abbr.BirId</code > class allows us to represent
      not just single forms but hybrids and species pairs;
      refer to <ulink url='http://www.nmt.edu/~shipman/xnomo'
      ><citetitle >A system for representing bird
      taxonomy</citetitle ></ulink > for the details of the
      bird code system.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .birdId:
          [ an abbr.BirdId instance representing (ab6, rel, alt) ]
</programlisting>
    <para>
      Sightings are added and retrieving using these methods:
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .__len__(self):  [ return number of sightings in self ]
        .__getitem__(self, n):
          [ n is an integer ->
              if n is the index of a sighting in self ->
                return that as a Sighting instance
              else -> raise KeyError ]
        .addSighting ( sighting ):
          [ sighting is a Sighting instance ->
              self  :=  self with sighting added ]
        .genSightings():
          [ generate sightings in self as a sequence of Sighting
            instances in the order they were added ]
</programlisting>
    <para>
      Because the schema allows elements such as <code
      >loc-detail</code > and <code >desc</code > to be attached
      to either the <code >form</code > element or the <code
      >floc</code > element, we have to implement inheritance.
      For example, if the parent <code >form</code > has a <code
      >loc-detail</code > describing the precise location of the
      sighting, it applies to each of its child <code >floc</code
      > elements, <emphasis >unless</emphasis > the child
      overrides it with its own <code >loc-detail</code >
      element.  Hence, these next attributes represent items that
      can occur either here in a <code >BirdForm</code > instance
      or in the child <code >Sighting</code > instances or both.
      They are designated as read/write attributes, intended to
      be set or retrieved by direct attribute references from
      outside the class.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .locGroup:
          [ if self has locality information ->
              a LocGroup instance representing the locality
            else -> None ]
        .sightNotes:
          [ if self has any sighting notes ->
              a SightNotes instance representing those notes
            else -> None ]
</programlisting>
    <para>
      We also provide a <code >.getLoc()</code > method to
      find the effective locality data, using inheritance from
      the parent <code >DayNotes</code > instance for locality
      attributes not defined at this level.  Compare this to
      the <code >.locGroup</code > attribute, which describes
      only the locality data directly provided at the <code
      >form</code > level.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        .getLoc():
          [ return the effective locality data for self as a
            Loc instance ]
</programlisting>
    <para>
      The class has the usual node reader and writer functions.
      There may be one or multiple &#x201c;sightings,&#x201d;
      stored in the instance's <code >.__sightingList</code >
      attribute.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        BirdForm.readNode ( txny, dayNotes, node ):    # Static method
          [ (txny is a bird taxonomy as a txny.Txny instance) and
            (dayNotes is the parent DayNotes instance) and
            (node is an et.Element) ->
              if node is an rnc.FORM_N node conforming to
              &rnc; ->
                return a new BirdForm instance representing node
              else -> raise IOError ]                
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with a new rnc.FORM_N node
                          added representing self
              return that new node ]

</programlisting>
      <para>
        Additional internal state:
      </para>
      <programlisting role='outFile:birdnotes.py'
>      State/Invariants:
        .__sightingList:
          [ a list containing the Sighting instances in self
            in the order they were added ]
    """
</programlisting>
    <section id='BirdForm-len'>
      <title><code >BirdForm.__len__()</code >: Number of
      sightings</title>
      <para>
        This method returns the number of sightings inside the instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . _ _ l e n _ _

    def __len__ ( self ):
        """Return the sighting count for self.
        """
        return  len ( self.__sightingList )
</programlisting>
    </section> <!--BirdForm-len-->
    <section id='BirdForm-getitem'>
      <title><code >BirdForm.__getitem__()</code >: Index function</title>
      <para>
        This function retrieves the <replaceable >n</replaceable
        >th sighting in <code >self</code >, or raises <code
        >KeyError</code > otherwise.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . _ _ g e t i t e m _ _

    def __getitem__ ( self, n ):
        """Return the (n)th sighting in self
        """
        return  self.__sightingList[n]
</programlisting>
    </section> <!--BirdForm-getitem-->
    <section id='BirdForm-addSighting'>
      <title><code >BirdForm.addSighting()</code ></title>
      <para>
        This method adds one <code >Sighting</code > instance to the
        internal <code >.__sightingList</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . a d d S i g h t i n g

    def addSighting ( self, sighting ):
        """Add a sighting to self.
        """
        self.__sightingList.append ( sighting )
</programlisting>
    </section> <!--BirdForm-addSighting-->
    <section id='BirdForm-genSightings'>
      <title><code >BirdForm.genSightings()</code ></title>
      <para>
        Generates the sightings in the order they were added.  This can
        be implemented by returning an iterator for the <code
        >.__sightingList</code > attribute, which contains the
        sightings.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . g e n S i g h t i n g s

    def genSightings ( self ):
        """Generate the sightings in self.
        """
        return  iter ( self.__sightingList )
</programlisting>
    </section> <!--BirdForm-genSightings-->
    <section id='BirdForm-init'>
      <title><code >BirdForm.__init__()</code ></title>
      <para>
        All the constructor does is save its arguments and
        establish the class invariants.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . _ _ i n i t _ _

    def __init__ ( self, txny, dayNotes, ab6, rel=None, alt=None,
                   notable=1 ):
        """Constructor for BirdForm
        """

        #-- 1 --
        self.txny  =  txny
        self.dayNotes  =  dayNotes
        self.ab6  =  ab6
        self.rel  =  rel
        self.alt  =  alt
        self.notable  =  notable
        self.locGroup  =  None
        self.sightNotes  =  None
        self.__sightingList  =  []
</programlisting>
      <para>
        We must check that the <code >ab6</code > code, and the <code
        >alt</code > code if used, are valid in the current code system.
        All this checking is handled in the <code
        >abbrModule.BirdId()</code > constructor.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if (ab6, rel, alt) are valid in txny ->
        #     self.birdId  :=  a BirdId instance representing
        #                      that bird identity
        #   else ->
        #     raise IOError ]
        try:
            self.birdId  =  abbrModule.BirdId ( txny, ab6, rel, alt )
        except KeyError:
</programlisting>
      <para>
        This next line is just a bit obscure.  It should be read:
        &#x201c;set <code >offender</code > to the concatenation of:
        <code >ab6</code >; <code >rel</code >, or an empty string if
        <code >rel</code > is false; and <code >alt</code >, or an empty
        string if <code >alt</code > is false.&#x201d;  For example,
        for a hybrid of Black Duck and Northern Shoveler, this would
        set <code >offender</code > to <code >"blkduc^norsho"</code >,
        but for a Wrentit record we'd get simply <code >"wrenti"</code
        >, and not annoy Python by trying to concate <code >None</code >
        with a string value.
      </para>
      <programlisting role='outFile:birdnotes.py'
>            offender  =  ( "%s%s%s" % (ab6, rel or "", alt or "") )
            raise IOError, ( "Code '%s' undefined." % offender ) 
</programlisting>
    </section> <!--BirdForm-init-->
    <section id='BirdForm-getLocGroup'>
      <title><code >BirdForm.getLocGroup()</code >: Find the
      effective locality</title>
      <para>
        This method uses inheritance to find the effective
        locality data.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . g e t L o c G r o u p

    def getLocGroup ( self ):
        """Get the effective locality.
        """
</programlisting>
      <para>
        First we get the effective default locality for the day.
        See <xref linkend='DayNotes-defaultLoc' />, which
        returns a location code as a string, and <xref
        linkend='class-LocGroup' /> for the constructor that
        wraps that in a <code >LocGroup</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ parentGroup  :=  a LocGroup instance representing the
        #       default locality for self.dayNotes ]
        parentGroup  =  LocGroup ( self.dayNotes.defaultLoc() )
</programlisting>
      <para>
        If there is no locality data at this level, we'll just return
        the parent's locality.  Otherwise, we'll use the <code
        >LocGroup.inherit()</code > method to build a new <code
        >LocGroup</code > instance with default values replaced by
        inherited values .  See <xref linkend='LocGroup-inherit' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if self.locGroup is None ->
        #     return parentGroup
        #   else ->
        #     return a new LocGroup instance made from
        #         self.locGroup inheriting from parentGroup ]
        if  self.locGroup:
            return  self.locGroup.inherit ( parentGroup )
        else:
            return  parentGroup
</programlisting>
    </section> <!--BirdForm-getLocGroup-->
    <section id='BirdForm-readNode'>
      <title><code >BirdForm.readNode()</code > (static
      method)</title>
      <para>
        As discussed in the introduction to <xref
        linkend='class-BirdForm' />, the translation of XML into
        an internal data structure does not strictly follow the
        structure of the XML: a <code >form</code > element with
        no <code >floc</code > children will be represented as a
        <code >BirdForm</code > instance with <emphasis
        >one</emphasis > <code >Sighting</code > instance, while
        if there are <code >floc</code > children, each of them
        will be converted to one <code >Sighting</code >
        instance.
      </para>
      <para>
        This method requires a <code >txny</code > instance that
        defines the taxonomy of birds, and also the parent <code
        >dayNotes</code > instance so that it can check the
        validity of any location codes used.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . r e a d N o d e

#   @staticmethod
    def readNode ( txny, dayNotes, node ):
        """Convert from XML
        """
</programlisting>
      <para>
        The first step is to extract the parts of the <code
        >form</code > element: the <code >taxon-group</code >
        attributes, the optional <code >loc-group</code > pattern
        (which includes child <code >loc-detail</code > elements
        as well as attributes), and the various <code
        >sighting-notes</code > child elements.  See <xref
        linkend='BirdForm-getTaxonGroup' />, which also builds
        the basic <code >BirdForm</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ birdForm  :=  a BirdForm instance with parent dayNotes
        #       made from node's taxon-group ]
        birdForm  =  BirdForm.getTaxonGroup ( txny, dayNotes, node )
</programlisting>
      <para>
        At this point we determine whether there any <code
        >floc</code > children or not.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ flocList  :=  node's rnc.FLOC_N children as et.Element
        #                 instances ]
        flocList  =  node.xpath ( rnc.FLOC_N )
</programlisting>
      <para>
        At this point, if <code >flocList</code > is empty, this is the
        single-sighting case; otherwise it is the multi-sighting case.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if flocList is empty ->
        #     birdForm  :=  birdForm with a single Sighting child
        #         added, made from node's age-sex-group, loc-group,
        #         and sighting-notes content
        #   else ->
        #     birdForm  :=  birdForm with loc-group and sighting-notes
        #         added from node, and Sighting children added, made
        #         from the elements of flocList ]
        if  len(flocList) == 0:
            birdForm.singleSighting ( dayNotes, node )
        else:
            birdForm.multiSighting ( dayNotes, node, flocList )

        #-- 4 --
        return birdForm

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--BirdForm-readNode-->
    <section id='BirdForm-getTaxonGroup'>
      <title><code >BirdForm.getTaxonGroup()</code > (static
      method)</title>
      <para>
        The purpose of this method is to extract all the items of the
        <code >taxon-group</code > pattern from the XML <code
        >form</code > element, and use that to construct a new <code
        >BirdForm</code > instance.  It is a static method because it is
        called from <xref linkend='BirdForm-readNode' />, which is also
        a static method.
      </para>
      <para>
        Refer to the schema for the <ulink
        url='&spec;rnc-taxon-group.html' >definition of <code
        >taxon-group</code ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . g e t T a x o n G r o u p

#   @staticmethod
    def getTaxonGroup ( txny, dayNotes, node ):
        """Convert the XML taxon-group pattern to a BirdForm instance.

          [ (txny is a bird taxonomy as a txny.Txny instance) and
            (dayNotes is the parent DayNotes instance) and
            (node is an et.Element) ->
              return a new BirdForm instance made from node's
              taxon-group content ]
        """
</programlisting>
      <para>
        First we extract the <code >taxon-group</code > items.  The
        <code >ab6</code > attribute is required; <code >rel</code >,
        <code >alt</code >, and <code >notable</code > are optional.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ ab6  :=  node's rnc.AB6_A attribute
        #   rel  :=  node's rnc.REL_A attribute, defaulting to None
        #   alt  :=  node's rnc.ALT_A attribute, defaulting to None
        #   notable  :=  node's rnc.NOTABLE_A attribute, defaulting
        #                to None ]
        ab6  =  node.attrib [ rnc.AB6_A ]
        rel  =  node.attrib.get ( rnc.REL_A, None )
        alt  =  node.attrib.get ( rnc.ALT_A, None )
        notable  =  node.attrib.get ( rnc.NOTABLE_A, None )
</programlisting>
      <para>
        At this point we have everything we need to build a new <code
        >BirdForm</code > instance; see <xref linkend='BirdForm-init'
        /> for the constructor's calling sequence.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ birdForm  :=  a new BirdForm instance using ab6, rel,
        #       alt, and notable ]
        birdForm  =  BirdForm ( txny, dayNotes, ab6, rel, alt,
                                notable )
</programlisting>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        return birdForm

    getTaxonGroup  =  staticmethod(getTaxonGroup)
</programlisting>
    </section> <!--BirdForm-getTaxonGroup-->
    <section id='BirdForm-singleSighting'>
      <title><code >BirdForm.singleSighting()</code >: Read a
      single sighting</title>
      <para>
        This method builds a <code >Sighting</code > instance
        from a <code >form</code > element when there are no
        child <code >floc</code > elements.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . s i n g l e S i g h t i n g

    def singleSighting ( self, dayNotes, node ):
        """Build a Sighting child for the single-sighting case.

          [ (dayNotes is the parent DayNotes instance) and
            (node is an rnc.FORM_N node) ->
              self  :=  self with a single Sighting child added,
                  made from node's age-sex-group, loc-group, and
                  sighting-notes content ]
        """
</programlisting>
      <para>
        See <xref linkend='Sighting-init' /> for the constructor.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ sighting  :=  a new, empty Sighting instance with
        #                 self as the parent ]
        sighting  =  Sighting ( self )
</programlisting>
      <para>
        Next, check for <code >loc-group</code > content.  If there is
        any, and it has a <code >loc</code > attribute, 
we'll also need
        to be sure that location code is defined.  See <xref
        linkend='LocGroup-readNode' /> and <xref
        linkend='DayNotes-lookupLoc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if node has any valid loc-group content ->
        #     sighting.locGroup  :=  that content as a LocGroup
        #                            instance
        #   else if node loc-group content with a loc that is
        #   not in dayNotes ->
        #     raise IOError
        #   else ->
        #     sighting.locGroup  :=  None ]
        sighting.locGroup  =  LocGroup.readNode ( node, dayNotes )
</programlisting>
      <para>
        The other two kinds of content proceed similarly.  See <xref
        linkend='AgeSexGroup-readNode' /> and <xref
        linkend='SightNotes-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if  node has any age-sex-group content ->
        #     sighting.ageSexGroup  :=  a new AgeSexGroup
        #         instance representing that content
        #   else ->
        #     sighting.ageSexGroup  :=  None ]
        sighting.ageSexGroup  =  AgeSexGroup.readNode ( node )

        #-- 4 --
        # [ if node has any sighting-notes content ->
        #     sighting.sightNotes  :=  a new SightNotes instance
        #         representing that content
        #   else ->
        #     sighting.sightNotes  :=  None ]
        sighting.sightNotes  =  SightNotes.readNode ( node )
</programlisting>
      <para>
        To add the new child sighting, see <xref
        linkend='BirdForm-addSighting' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ self  :=  self with sighting added ] 
        self.addSighting(sighting)
</programlisting>
    </section> <!--BirdForm-singleSighting-->
    <section id='BirdForm-multiSighting'>
      <title><code >BirdForm.multiSighting()</code >: Read the
      multi-sighting case</title>
      <para>
        While reading an XML file, this method handles the case where
        the <code >form</code > element has one or more <code
        >floc</code > children.
      </para>
      <para>
        Note that <code >loc-group</code > and <code
        >sighting-notes</code > content can occur <emphasis
        >both</emphasis > at the <code >form</code > level and at the
        <code >floc</code > level.  The former is attached to
        the <code >BirdForm</code > instance, while the latter is
        attached to the child <code >Sighting</code > instance.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . m u l t i S i g h t i n g

    def multiSighting ( self, dayNotes, formNode, flocList ):
        """Read a form with floc children.

          [ (dayNotes is the parent DayNotes instance) and
            (formNode is an rnc.FORM_N et.Element) and
            (flocList is a list of its rnc.FLOC_N children) ->
              self  :=  self with loc-group and sighting-notes
                  added from node, and Sighting children added, made
                  from the elements of flocList ]
        """
</programlisting>
      <para>
        If there is no <code >loc-group</code > content, set <code
        >self.locGroup</code > to <code >None</code >, otherwise set it
        to a <code >LocGroup</code > instance.  See <xref
        linkend='LocGroup-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if formNode has any loc-group content ->
        #     self.locGroup  :=  that content as a LocGroup instance
        #   else ->
        #     self.locGroup  :=  None ]
        self.locGroup  =  LocGroup.readNode ( formNode, dayNotes )
</programlisting>
      <para>
        The parent node's <code >sighting-notes</code > is handled
        similarly; see <xref linkend='SightNotes-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if formNode has any sighting-notes content ->
        #     self.sightNotes  :=  that content as a SightNotes
        #                          instance
        #   else ->
        #     self.sightNotes  :=  None ]
        self.sightNotes  =  SightNotes.readNode ( formNode )
</programlisting>
      <para>
        Each child <code >floc</code > node is converted into a child
        <code >Sighting</code > by <xref linkend='BirdForm-readFloc' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ self  :=  self with child Sighting instances added
        #             made from the elements of flocList ]
        for  flocNode in flocList:
            self.readFloc ( flocNode, dayNotes )
</programlisting>
    </section> <!--BirdForm-multiSighting-->
    <section id='BirdForm-readFloc'>
      <title><code >BirdForm.readFloc()</code >: Read one <code
      >floc</code > element</title>
      <para>
        This method converts a <code >floc</code > node into a new <code
        >Sighting</code > instance and adds it as the next child
        of <code >self</code >.
      </para>
      <para>
        The content of the <code >floc</code > element is the <code
        >single-sighting</code > pattern; refer to the <ulink
        url='&spec;rnc-form.html' >schema</ulink > for the relevant
        definitions.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . r e a d F l o c

    def readFloc ( self, flocNode, dayNotes ):
        """Read one floc element.

          [ (flocNode is an rnc.FLOC_N node as an et.Element) and
            (dayNotes is a DayNotes instance) ->
              self  :=  self with a Sighting instance added, made
                        from flocNode, with parent dayNotes ]
        """
</programlisting>
      <para>
        First we construct a <code >Sighting</code > to hold
        the content.  See <xref linkend='Sighting-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ sighting  :=  a new, empty Sighting instance with
        #                 self as its parent ]
        sighting  =  Sighting ( self )
</programlisting>
      <para>
        Next we check for the various types of content that
        can occur in a <code >floc</code > element:
        <code >age-sex-group</code >, <code >loc-group</code >,
        and <code >sighting-notes</code >.  See
        <xref linkend='AgeSexGroup-readNode' />,
        <xref linkend='LocGroup-readNode' />, and
        <xref linkend='SightNotes-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if flocNode has any age-sex-group content ->
        #     sighting.ageSexGroup  :=  an AgeSexGroup instance
        #         representing that content
        #   else ->
        #     sighting.ageSexGroup  :=  None ]
        sighting.ageSexGroup  =  AgeSexGroup.readNode ( flocNode )

        #-- 3 --
        # [ if flocNode has any loc-group content ->
        #     sighting.locGroup  :=  a LocGroup instance
        #         representing that content
        #   else ->
        #     sighting.locGroup  :=  None ]
        sighting.locGroup  =  LocGroup.readNode ( flocNode, dayNotes )

        #-- 4 --
        # [ if flocNode has any sighting-notes content ->
        #     sighting.sightNotes  :=  a SightNotes instance
        #         representing that content
        #   else ->
        #     sighting.sightNotes  :=  None ]
        sighting.sightNotes  =  SightNotes.readNode ( flocNode )
</programlisting>
      <para>
        Finally, add the new <code >Sighting</code > instance to <code
        >self</code >.  See <xref linkend='BirdForm-addSighting' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        # [ self  :=  self with sighting added ]
        self.addSighting ( sighting )
</programlisting>
    </section> <!--BirdForm-readFloc-->
    <section id='BirdForm-writeNode'>
      <title><code >BirdForm.writeNode()</code >: Translate to
      XML</title>
      <para>
        This method translates a <code >BirdForm</code > instance to
        XML, building the <code >form</code > node and its subtree.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . w r i t e N o d e

    def writeNode ( self, parent ):
        """Add self's content to the parent node.
        """
        #- 1 --
        # [ parent  :=  parent with a new rnc.FORM_N node added
        #   formnode  :=  that new node ]
        formNode  =  et.SubElement ( parent, rnc.FORM_N )
</programlisting>
      <para>
        We first add the <code >taxon-group</code
        > content to the node we are building; see <xref
        linkend='BirdForm-writeTaxonGroup' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ formNode  +:=  self's taxon-group content ]
        self.__writeTaxonGroup ( formNode )
</programlisting>
      <para>
        As with <xref linkend='BirdForm-readNode' />, there are two
        general cases, depending on whether there are multiple sightings
        or not.  See <xref linkend='BirdForm-writeSingle' /> and <xref
        linkend='BirdForm-writeMulti' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if self has only one sighting ->
        #     formNode  +:=  form node with age-sex-group, loc-group,
        #         and sighting-notes content added from that sighting
        #   else ->
        #     formNode  :=  formNode + (loc-group and sighting-notes
        #         from self) + (rnc.FLOC_N children added made from
        #         self's sightings ]
        if  self.nSightings() == 1:
            self.writeSingle ( formNode )
        else:
            self.writeMulti ( formNode )
</programlisting>
    </section> <!--BirdForm-writeNode-->
    <section id='BirdForm-writeTaxonGroup'>
      <title><code >BirdForm.__writeTaxonGroup()</code ></title>
      <para>
        This method adds the <code >taxon-group</code > content to the
        XML being built.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . _ _ w r i t e T a x o n G r o u p

    def __writeTaxonGroup ( self, formNode ):
        """Attach taxon-group content to formNode.

          [ formNode is an et.Element ->
              formNode  +:=  self's taxon-group content ]
        """
</programlisting>
      <para>
        The <code >ab6</code > attribute is required.  The <code
        >rel</code > attribute is optional, but if it is used, there
        must also be an <code >alt</code > attribute.  Finally, the
        <code >notable</code > attribute is optional.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ formNode  +:=  an rnc.AB6_A attribute made from self.ab6 ]
        formNode.attrib [ rnc.AB6_A ]  =  self.ab6

        #-- 2 --
        # [ if self.rel ->
        #     formNode  :=  formNode with an rnc.REL_A attribute
        #         made from self.rel and an rnc.ALT_A attribute
        #         made from self.alt
        #   else -> I ]
        if  self.rel:
            formNode.attrib [ rnc.REL_A ]  =  self.rel
            formNode.attrib [ rnc.ALT_A ]  =  self.alt

        #-- 3 --
        if  self.notable:
            formNode.attrib [ rnc.NOTABLE_A ]  =  self.notable
</programlisting>
    </section> <!--BirdForm-writeTaxonGroup-->
    <section id='BirdForm-writeSingle'>
      <title><code >BirdForm.writeSingle()</code >: Create
      single-sighting XML</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . w r i t e S i n g l e

    def writeSingle ( self, formNode ):
        """Generate XML for a single sighting.

          [ formNode is an et.Element ->
              formNode  +:=  form node with age-sex-group, loc-group,
                  and sighting-notes content added from that sighting ]
        """

        #-- 1 --
        sighting  =  self.__sightingList[0]
</programlisting>
      <para>
        For each of the three content groups, we add the XML using
        the appropriate <code >.writeNode()</code > method.  See
        <xref linkend='AgeSexGroup-writeNode' />,
        <xref linkend='LocGroup-writeNode' />, and
        <xref linkend='SightNotes-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if sighting.ageSexGroup ->
        #     formNode  :=  formNode with age-sex-group content
        #                   added from sighting
        #   else -> I ]
        if  sighting.ageSexGroup:
            sighting.ageSexGroup.writeNode ( formNode )

        #-- 2 --
        # [ if sighting.locGroup ->
        #     formNode  :=  formNode with loc-group content
        #                   added from sighting
        #   else -> I ]
        if  sighting.locGroup:
            sighting.locGroup.writeNode ( formNode )

        #-- 3 --
        # [ if sighting.sightNotes ->
        #     formNode  :=  formNode with sighting-notes content
        #                   added from sighting
        #   else -> I ]
        if  sighting.sightNotes:
            sighting.sightNotes.writeNode ( formNode )
</programlisting>
    </section> <!--BirdForm-writeSingle-->
    <section id='BirdForm-writeMulti'>
      <title><code >BirdForm.writeMulti()</code >: Create multi-sighting
      XML</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d F o r m . w r i t e M u l t i

    def writeMulti ( self, formNode ):
        """Generate XML for multiple sightings.

          [ formNode is an et.Element ->
              formNode  :=  formNode + (loc-group and sighting-notes
                  from self) + (rnc.FLOC_N children made from
                  self's sightings ]
        """
</programlisting>
      <para>
        For the logic that adds the <code >loc-group</code > and
        <code >sighting-notes</code > content,
        see <ulink url='LocGroup-writeNode' ></ulink > and
        <ulink url='SightNotes-writeNode' ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>
        #-- 1 --
        # [ if self.locGroup ->
        #     formNode  +:=  self.locGroup as XML
        #   else -> I ]
        if  self.locGroup:
            self.locGroup.writeNode ( formNode )

        #-- 2 --
        # [ if self.sightNotes ->
        #     formNode  +:=  self.sightNotes as XML
        #   else -> I ]
        if  self.sightNotes:
            self.sightNotes.writeNode ( formNode )
</programlisting>
      <para>
        Translate each child <code >Sighting</code > instance
        into XML; see <ulink url='Sighting-writeNode' ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ formNode  +:=  rnc.FLOC_N children made from self's
        #                  sightings ]
        for  sighting in self.genSightings():
            sighting.writeNode ( formNode )
</programlisting>
    </section> <!--BirdForm-writeMulti-->
  </section> <!--class-BirdForm-->
  <section id='class-Sighting'>
    <title><code >class Sighting</code >: Sighting group</title>
    <para>
      An instance of this class may represent either a <code
      >form</code > element with no <code >floc</code > children,
      or one <code >floc</code > child.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   S i g h t i n g

class Sighting:
    """Represents one sighting of one or more similar birds.

      Exports:
        Sighting ( birdForm ):
          [ birdForm is a BirdForm instance ->
              return a new, empty Sighting instance with
              birdForm as its parent ]
        .birdForm:    [ as passed to constructor, read-only ]
        .locGroup:
          [ if self has locality content ->
              a LocGroup instance representing that content
            else -> None ]
        .ageSexGroup:
          [ if self has any age-sex-group content ->
              an AgeSexGroup instance representing that content
            else -> None ]
        .sightNotes:
          [ if self has any sighting-notes content ->
              a SightNotes instance representing that content
            else -> None ]
        .getLocGroup():
          [ return self's locality as a LocGroup object ]
</programlisting>
      <para>
        This method is used to translate a sighting to XML.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        .writeNode ( formNode ):
          [ formNode is an et.Element ->
              formNode  +:=  a new rnc.FLOC_N child made from self ]
    """
</programlisting>
    <section id='Sighting-init'>
      <title><code >Sighting.__init__()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t i n g . _ _ i n i t _ _

    def __init__ ( self, birdForm ):
        """Constructor for Sighting.
        """
        self.birdForm  =  birdForm
        self.locGroup  =  None
        self.ageSexGroup  =  None
        self.sightNotes  =  None
</programlisting>
    </section> <!--Sighting-init-->
    <section id='Sighting-getLocGroup'>
      <title><code >Sighting.getLocGroup()</code >: Find a
      sighting's effective locality</title>
      <para>
        The purpose of this method is to find out where a
        sighting occurred.  Due to inheritance, however, the
        effective location may live in the <code >Sighting</code
        > instance, or in its parent <code >BirdForm</code >, or
        it may be the default locality for the day, which resides
        in the related <code >DaySummary</code >.
      </para>
      <para>
        While working on an earlier version of this module, the
        author became embroiled in an interesting discussion
        involving the <ulink
        url='http://en.wikipedia.org/wiki/Law_of_Demeter' >Law of
        Demeter</ulink >.  Consider the case where a sighting
        does not define a location code, so that it inherits its
        location code from the <code >DaySummary</code >.  How
        are we to find out the day's default location?
      </para>
      <para>
        In the original version (with different names), this ugly
        code I wrote drew fire from colleagues:
      <programlisting
>        locCode  =  None
        if  self.locGroup is not None:
            locCode  =  self.locGroup.locCode
        if  locCode is None:
            if self.birdForm.locGroup is not None:
                locCode  =  self.birdForm.locGroup.locCode
        if  locCode is None:
            locCode  =  self.birdForm.dayNotes.daySummary.defaultLoc
</programlisting>
        That is, if the sighting defines a location code,
        use that; if not, and the parent <code >form</code >
        defines a location code, use that; otherwise, go up to the
        <code >form</code > element's parent <code
        >day-notes</code >, then down to that element's
        <code >day-summary</code >, and dig out the default
        location code.
      </para>
      <para>
        However, this violates the Law of Demeter, which in the
        succinct form quoted in the Wikipedia article above, says
        &#x201c;Only talk to your immediate friends.&#x201d;
        Dr. Allan Stavely suggested a much cleaner approach:
      </para>
      <orderedlist>
        <listitem>
          <para>
            If the <code >Sighting</code > instance defines a
            location, use that.
          </para>
          <para>
            If the <code >Sighting</code > has no location code, ask the
            parent <code >BirdForm</code > instance what location code
            it uses.  In this code, that uses the <code
            >BirdForm.getLocGroup()</code > method.
          </para>
        </listitem>
        <listitem>
          <para>
            The <code >BirdForm.getLocGroup()</code > method uses its
            own <code >.locGroup</code > value if there is one; if
            not, it interrogates the parent <code
            >DayNotes.defaultLoc()</code > method for the
            default location.
          </para>
        </listitem>
        <listitem>
          <para>
            The <code >DayNotes.defaultLoc()</code > function is
            a pass-through that interrogates its <code
            >daySummary.defaultLoc()</code > method.
          </para>
        </listitem>
      </orderedlist>
      <para>
        The payoff here is that the <code >Sighting</code >
        element does not need to know anything about the
        internals of two classes that are not its immediate
        neighbors: <code >DayNotes</code > and <code >DaySummary</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t i n g . g e t L o c G r o u p

    def getLocGroup ( self ):
        """Find self's effective location.
        """
</programlisting>
      <para>
        First we ask the parent <code >BirdForm</code > instance for a
        <code >LocGroup</code > that describes the locality from which
        we may inherit.  See <xref linkend='BirdForm-getLocGroup' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ parentGroup  :=  a LocGroup representing the
        #       effective locality of self.birdForm ]
        parentGroup  =  self.birdForm.getLocGroup()
</programlisting>
      <para>
        If  <code >self.locGroup</code > is <code >None</code >, 
        we'll just return the parent locality.  Otherwise we use
        inheritance to form the sighting's effective locality;
        see <xref linkend='LocGroup-inherit' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if self.locGroup is None ->
        #     return parentGroup
        #   else ->
        #     return a new LocGroup made from self.locGroup,
        #     inheriting from parentGroup ]
        if  self.locGroup is None:
            return parentGroup
        else:
            return self.locGroup.inherit ( parentGroup )
</programlisting>
    </section> <!--Sighting-getLocGroup-->
    <section id='Sighting-writeNode'>
      <title><code >Sighting.writeNode()</code >: Translate to
      XML</title>
      <para>
        This method converts <code >self</code > to a new <code
        >floc</code > node.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t i n g . w r i t e N o d e

    def writeNode ( self, formNode ):
        """Convert self to XML.

          [ formNode is an et.Element ->
              formNode  +:=  a new rnc.FLOC_N child made from self ]
        """
</programlisting>
      <para>
        First, attach the new node.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ formNode  :=  formNode with a new rnc.FLOC_N child added
        #   flocNode  :=  that new child ]
        flocNode  =  et.SubElement ( formNode, rnc.FLOC_N )
</programlisting>
      <para>
        There are three content groups, each with a corresponding
        method that translates it to XML.
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            For <code >age-sex-group</code >, see <xref
            linkend='AgeSexGroup-writeNode' />.
          </para>
        </listitem>
        <listitem>
          <para>
            For <code >loc-group</code >, see <xref
            linkend='LocGroup-writeNode' />.
          </para>
        </listitem>
        <listitem>
          <para>
            For <code >sighting-notes</code >, see <xref
            linkend='SightNotes-writeNode' />.
          </para>
        </listitem>
      </itemizedlist>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if self.ageSexGroup ->
        #     flocNode  +:=  age-sex-group content from
        #                    self.ageSexGroup
        #   else -> I ]
        if  self.ageSexGroup:
            self.ageSexGroup.writeNode ( flocNode )

        #-- 3 --
        # [ simile ]
        if  self.locGroup:
            self.locGroup.writeNode ( flocNode )

        #-- 4 --
        if  self.sightNotes:
            self.sightNotes.writeNode ( flocNode )
</programlisting>
    </section> <!--Sighting-writeNode-->
  </section> <!--class-Sighting-->
  <section id='class-LocGroup'>
    <title><code >class LocGroup</code >: Sighting's locality</title>
    <para>
      An instance of this class represents all the content
      defined in the <code >loc-group</code > pattern.  See the
      <ulink url='&spec;rnc-loc-group.html' >definition of the
      <code >loc-group</code > pattern in the schema</ulink >.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   L o c G r o u p

class LocGroup:
    """Represents locality information for sightings.

      Exports:
        LocGroup ( loc=None, gps=None, locDetail=None ):
          [ (loc is a locality as a Loc instance or None) and
            (gps is a Gps instance or None) and
            (locDetail is a Narrative instance or None) ->
              return a new LocGroup instance with those values ]
        .loc:          [ as passed to constructor, read-only ]
        .gps:          [ as passed to constructor, read-only ]
        .locDetail:    [ as passed to constructor, read-only ]
        LocGroup.readNode ( node, dayNotes ):   # Static method
          [ (node is an et.Element) and
            (dayNotes is a DayNotes instance) ->
              if node has any valid loc-group content ->
                return a new LocGroup instance representing that
                content
              else if node has loc-group content but there are
              any location codes undefined in dayNotes ->
                raise IOError
              else -> return None ]
        .inherit ( parentGroup ):
          [ parentGroup is a LocGroup instance ->
              return a new LocGroup whose attributes come from
              parentGroup except when they are overriden by
              corresponding attributes of self ]
        .writeNode ( node ):
          [ node is an et.Element ->
              node  :=  node with self added as XML ]
    """
</programlisting>
    <section id='LocGroup-init'>
      <title><code >LocGroup.__init__()</code ></title>
      <para>
        None of the three component attributes&#x2014;location
        code, GPS waypoint, or location detail&#x2014;are
        required.  We allow each attribute to be optional in order
        to implement inheritance: each of these attributes may be
        inherited from a parent element.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c G r o u p . _ _ i n i t _ _

    def __init__ ( self, loc=None, gps=None, locDetail=None ):
        """Constructor for LocGroup
        """
        self.loc  =  loc
        self.gps  =  gps
        self.locDetail  =  locDetail
</programlisting>
    </section> <!--LocGroup-init-->
    <section id='LocGroup-readNode'>
      <title><code >LocGroup.readNode()</code >: Extract <code
      >loc-group</code > content</title>
      <para>
        This method finds content in and under a given <code >node</code
        > that matches the <code >loc-group</code > pattern in the
        <ulink url='&spec;rnc-loc-group.html' >schema</ulink >.  If
        that content includes a <code >loc</code > attribute, we will
        also check to make sure that location code has been defined;
        this is why the method requires a <code >DayNotes</code >
        instance that contains the location code definitions.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c G r o u p . r e a d N o d e

#   @staticmethod
    def readNode ( node, dayNotes ):
        """Extract loc-group content from a node.
        """
</programlisting>
      <para>
        First we look for a <code >loc</code > attribute and, if
        it is present, also make sure it is defined for that day.
        If not, we set <code >loc</code > to <code >None</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if node has an rnc.LOC_A attribute defined in dayNotes ->
        #     locCode  :=  that attribute
        #   else if node has an rnc.LOC_A attribute not defined in
        #   dayNotes ->
        #     raise IOError
        #   else ->
        #     locCode  :=  None ]
        locCode  =  node.attrib.get ( rnc.LOC_A, None )

        #-- 2 --
        # [ if dayNotes has a definition for location code locCode ->
        #     loc  :=  that definition as a Loc instance
        #   else ->
        #     loc  :=  None ]
        if  locCode is not None:
            try:
                loc  =  dayNotes.lookupLoc ( locCode )
            except KeyError:
                raise IOError, ( "Location code '%s' is used but "
                    "not defined for date %s." %
                    (locCode, dayNotes.date) )
        else:
            loc  =  None
</programlisting>
      <para>
        Similarly, we try to get the <code >gps</code > attribute, and
        default its value to <code >None</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 -
        # [ gps  :=  node's rnc.GPS_A attribute, default None ]
        gps  =  node.attrib.get ( rnc.GPS_A, None )
</programlisting>
      <para>
        If the node has a <code >loc-detail</code > child, we retrieve
        the content of the child as a <code >Narrative</code > instance.
        See <xref linkend='Narrative-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if node has an rnc.LOC_DETAIL_N child ->
        #     locDetail  :=  the content of that child as a
        #                    Narrative instance
        #   else ->
        #     locDetail  :=  None ]
        detailChildList  =  node.xpath ( rnc.LOC_DETAIL_N )
        if  len(detailChildList) &gt; 0:
            locDetail  =  Narrative.readNode ( detailChildList[0] )
        else:
            locDetail  =  None
</programlisting>
      <para>
        At this point, if none of these three items exist, we
        can return <code >None</code > to the caller.  Otherwise
        we box them up into a <code >LocGroup</code > instance
        and return that.  See <xref linkend='LocGroup-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        if (loc or gps or locDetail):
            return LocGroup ( loc, gps, locDetail )
        else:
            return None

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--LocGroup-readNode-->
    <section id='LocGroup-inherit'>
      <title><code >LocGroup.inherit()</code >: Implement
      inheritance for locations</title>
      <para>
        This method is used to find the effective locality data
        when one element inherits locality from a parent element.
        For design notes on the inheritance process, see
        <xref linkend='Sighting-getLocGroup' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c G r o u p . i n h e r i t

    def inherit ( self, parentGroup ):
        """Implement locality inheritance.
        """
</programlisting>
      <para>
        Here, we make heavy use of Python's &#x201c;<code
        ><replaceable >A</replaceable > or <replaceable
        >B</replaceable ></code >&#x201d; operator to set a value to 
        <code ><replaceable >A</replaceable ></code > if <code
        ><replaceable >A</replaceable ></code > is not <code
        >None</code >, or to <code ><replaceable >B</replaceable
        ></code > if <code ><replaceable >A</replaceable ></code
        > is <code >None</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if self.loc is None ->
        #     loc  :=  parentGroup.loc
        #   else ->
        #     loc  :=  self.loc ]
        loc  =  self.loc or parentGroup.loc

        #-- 2 --
        # [ simile ]
        gps  =  self.gps or parentGroup.gps
        locDetail  =  self.locDetail or parentGroup.locDetail
</programlisting>
      <para>
        See <xref linkend='LocGroup-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        return LocGroup ( loc, gps, locDetail )
</programlisting>
    </section> <!--LocGroup-inherit-->
    <section id='LocGroup-writeNode'>
      <title><code >LocGroup.writeNode()</code >: Translate to
      XML</title>
      <para>
        This method adds <code >loc-group</code > content to an XML
        parent node.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   L o c G r o u p . w r i t e N o d e

    def writeNode ( self, node ):
        """Add loc-group content to node.
        """
</programlisting>
      <para>
        The <code >loc</code > and <code >gps</code > attributes are
        optional; add them only if they are not <code >None</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if self.locCode ->
        #     node  :=  node with an rnc.LOC_A attribute added
        #               from self.locCode
        #   else -> I ]
        if  self.loc:
            node.attrib [ rnc.LOC_A ]  =  self.loc.code

        #-- 2 --
        # [ if self.gps ->
        #     node  :=  node with an rnc.GPS_A attribute added
        #               from self.gps
        #   else -> I ]
        if  self.gps:
            node.attrib [ rnc.GPS_A ]  =  self.gps
</programlisting>
      <para>
        The <code >loc-detail</code > content is a child element, not an
        attribute.  The <code >.locDetail</code > attribute is a <code
        >Narrative</code > instance.  For the method that adds the <code
        >narrative</code > content, see <xref
        linkend='Narrative-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if  self.locDetail ->
        #     node  :=  node with a new child rnc.LOC_DETAIL_N
        #               node added made from self.locDetail ]
        if  self.locDetail:
            detail  =  et.SubElement ( node, rnc.LOC_DETAIL_N )
            self.locDetail.writeNode ( detail )
</programlisting>
    </section> <!--LocGroup-writeNode-->
  </section> <!--class-LocGroup-->
  <section id='class-AgeSexGroup'>
    <title><code >class AgeSexGroup</code >: Sighting core data</title>
    <para>
      This class represents content from the <code
      >age-sex-group</code > pattern in the schema.  See the
      <ulink url='&spec;rnc-age-sex-group.html' >definition of
      this pattern in the schema</ulink >.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   A g e S e x G r o u p

class AgeSexGroup:
    """Represents assorted details of birds sighted.

      Exports:
        AgeSexGroup ( age=None, sex=None, q=None, count=None,
                      fide=None ):
          [ (age is an age code or None) and
            (sex is a sex code or None) and
            (q is a countability flag or None) and
            (count is a count description string or None) and
            (fide is an attribution or None) ->
              return a new AgeSexGroup instance with those values ]
        AgeSexGroup.readNode ( node ):    # Static method
          [ node is an et.Element ->
              if node has any age-sex-group content ->
                return an AgeSexGroup instance representing that
                content
              else -> return None ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with self's age-sex-group
                  content attached ]
    """
</programlisting>
    <section id='AgeSexGroup-init'>
      <title><code >AgeSexGroup.__init__()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   A g e S e x G r o u p . _ _ i n i t _ _

    def __init__ ( self, age=None, sex=None, q=None, count=None,
                   fide=None ):
        """Constructor for AgeSexGroup.
        """
        self.age  =  age
        self.sex  =  sex
        self.q  =  q
        self.count  =  count
        self.fide  =  fide
</programlisting>
    </section> <!--AgeSexGroup-init-->
    <section id='AgeSexGroup-readNode'>
      <title><code >AgeSexGroup.readNode()</code > (static
      method)</title>
      <para>
        This method looks to see if the given <code >node</code > has
        any <code >age-sex-group</code > content and, if so, makes it
        into an <code >AgeSexGroup</code > instance and returns that.
        If there is no such content, it returns <code >None</code >.
        For the constructor, see <xref linkend='AgeSexGroup-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   A g e S e x G r o u p . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Check for age-sex-group content.
        """
        age  =  node.attrib.get ( rnc.AGE_A, None )
        sex  =  node.attrib.get ( rnc.SEX_A, None )
        q  =  node.attrib.get ( rnc.Q_A, None )
        count  =  node.attrib.get ( rnc.COUNT_A, None )
        fide  =  node.attrib.get ( rnc.FIDE_A, None )
        if  sex or age or q or count or fide:
            return AgeSexGroup ( age, sex, q, count, fide )
        else:
            return None

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--AgeSexGroup-readNode-->
    <section id='AgeSexGroup-writeNode'>
      <title><code >AgeSexGroup.writeNode()</code ></title>
      <para>
        Translate the instance back to XML.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   A g e S e x G r o u p . w r i t e N o d e

    def writeNode ( self, parent ):
        """Translate self to XML.
        """
        #-- 1 --
        # [ if self.age is true ->
        #     parent  :=  parent with an rnc.AGE_A attribute (self.age)
        #   else -> I ]
        if  self.age:
            parent.attrib [ rnc.AGE_A ]  =  self.age

        #-- 2 --
        # [ simile ]
        if  self.sex:
            parent.attrib [ rnc.SEX_A ]  =  self.sex

        #-- 3 --
        if  self.q:
            parent.attrib [ rnc.Q_A ]  =  self.q

        #-- 4 --
        if  self.count:
            parent.attrib [ rnc.COUNT_A ]  =  self.count

        #-- 5 --
        if  self.fide:
            parent.attrib [ rnc.FIDE_A ]  =  self.fide
</programlisting>
    </section> <!--AgeSexGroup-writeNode-->
  </section> <!--class-AgeSexGroup-->
  <section id='class-SightNotes'>
    <title><code >class SightNotes</code >: Optional notes</title>
    <para>
      This class is a container for all the content that can
      occur in the <code >sighting-notes</code > pattern in the
      schema.  For the actual definition of this pattern, see
      <ulink url='&spec;rnc-sighting-notes.html' >its definition
      in the schema</ulink >.
    </para>
    <para>
      Writeable instance attributes are used for the items
      that can occur singly.  For example, the <code >.desc</code
      > attribute is initially <code >None</code >; if there is a
      description, it is stored by code outside the class into
      that attribute as a <code >Narrative</code > instance.
      For content such as <code >Photo</code > that can occur any
      number of times, we provide methods to add these items.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   S i g h t N o t e s

class SightNotes:
    """Represents content from the sighting-notes pattern.

      Exports:
        SightNotes():
          [ return a new, empty SightNotes instance ]
        .desc:  [ description as a Narrative or None, read/write ]
        .behavior:
          [ behavior as a Narrative or None, read/write ]
        .voc:
          [ vocalizations as a Narrative or None, read/write ]
        .breeding:
          [ breeding evidence as a Narrative or None, read/write ]
        .addPhoto ( photo ):
          [ photo is a Photo instance ->
              self  :=  self with photo added ]
        .genPhotos():
          [ generate photos in self as a sequence of Photo instances ]
        .addPara ( para ):
          [ para is a Paragraph instance ->
              self  :=  self with para added to its general notes ]
        .notes:
          [ self's general notes as a Narrative instance ]
</programlisting>
      <para>
        The class also has the usual functions for reading
        and writing XML.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        SightNotes.readNode ( node ):   # Static
          [ node is an et.Element ->
              if node has any sighting-notes content ->
                return a new SightNotes instance with that content
              else -> return None ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with content added representing
                          self ]

      State/Invariants:
        .__photoList:
          [ list of Photo instances in the order they were added ]
    """
</programlisting>
    <section id='SightNotes-init'>
      <title><code >SightNotes.__init__()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . _ _ i n i t _ _

    def __init__ ( self ):
        """Constructor for SightNotes
        """
        self.desc  =  None
        self.behavior  =  None
        self.voc  =  None
        self.breeding  =  None
        self.notes  =  None
        self.__photoList  =  []
</programlisting>
    </section> <!--SightNotes-init-->
    <section id='SightNotes-addPhoto'>
      <title><code >SightNotes.addPhoto()</code ></title>
      <para>
        Instances of class <code >Photo</code > are accumulated
        in the <code >self.__photoList</code > list each time
        this method is called.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . a d d P h o t o

    def addPhoto ( self, photo ):
        """Add one photo reference.
        """
        self.__photoList.append ( photo )
</programlisting>
    </section> <!--SightNotes-addPhoto-->
    <section id='SightNotes-genPhotos'>
      <title><code >SightNotes.genPhotos()</code >: Generate
      photos</title>
      <para>
        This method returns an iterator that generates the <code
        >Photo</code > objects in self.  It can do this by simply
        returning and iterator over <code >self.__photoList</code
        >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . g e n P h o t o s

    def genPhotos ( self ):
        """Generate the photo objects in self.
        """
        return  iter(self.__photoList)
</programlisting>
    </section> <!--SightNotes-genPhotos-->
    <section id='SightNotes-addPara'>
      <title><code >SightNotes.addPara()</code >: Add a paragraph
      of notes</title>
      <para>
        The instance's <code >.notes</code > attribute is
        initially <code >None</code >.  If any notes are added,
        we create a <code >Narrative</code > instance, which is a
        container for <code >Paragraph</code > objects, and add
        the new paragraph to it.  See <xref linkend='Narrative-init' />
        and <xref linkend='Narrative-addPara' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . a d d P a r a

    def addPara ( self, para ):
        """Add a paragraph of notes.
        """
        #-- 1 --
        # [ if self.notes is None ->
        #     self.notes  :=  a new, empty Narrative instance
        #   else -> I ]
        if  self.notes is None:
            self.notes  =  Narrative()

        #-- 2 --
        # [ self.notes  :=  self.notes with para added as its new
        #                   last element ]
        self.notes.addPara ( para )
</programlisting>
    </section> <!--SightNotes-addPara-->
    <section id='SightNotes-readNode'>
      <title><code >SightNotes.readNode()</code > (static
      method)</title>
      <para>
        This method looks in a given <code >node</code > for any
        of the content from the <code >sighting-notes</code >
        pattern.  For the definition of the <code
        >sighting-notes</code > pattern, see <ulink
        url='&spec;rnc-sighting-notes.html'>the
        specification</ulink >.  Basically, we are looking for a
        set of items such as <code >desc</code > that can occur
        only once, and another set of items such as <code
        >photo</code > that can occur any number of times.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Extract sighting-tones from node, if any.
        """
</programlisting>
      <para>
        See <xref linkend='SightNotes-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ sightNotes  :=  a new, empty SightNotes instance ]
        sightNotes  =  SightNotes()
</programlisting>
      <para>
        The processing of the <code >narrative</code > content in each
        of the potential children is done by <xref
        linkend='Narrative-readChild' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if node has an rnc.DESC_N child ->
        #     sightNotes.desc  :=  the narrative content of that
        #         child as a Narrative instance
        #   else ->
        #     sightNotes.desc  :=  None ]
        sightNotes.desc  =  Narrative.readChild ( node, rnc.DESC_N )

        #-- 3 --
        # [ if node has an rnc.BEHAVIOR_N child ->
        #    sightNotes.behavior  :=  the narrative content of
        #        that child as a Narrative instance
        #   else ->
        #    sightNotes.behavior  :=  None ]
        sightNotes.behavior  =  Narrative.readChild ( node,
            rnc.BEHAVIOR_N )

        #-- 4 --
        # [ if node has an rnc.VOC_N child ->
        #    sightNotes.voc  :=  the narrative content of
        #        that child as a Narrative instance
        #   else ->
        #    sightNotes.voc  :=  None ]
        sightNotes.voc  =  Narrative.readChild ( node, rnc.VOC_N )

        #-- 5 --
        # [ if node has an rnc.BREEDING_N child ->
        #    sightNotes.breeding  :=  the narrative content of
        #        that child as a Narrative instance
        #   else ->
        #    sightNotes.breeding  :=  None ]
        sightNotes.breeding  =  Narrative.readChild ( node,
            rnc.BREEDING_N )
</programlisting>
      <para>
        There can be multiple <code >photo</code > children.  See <xref
        linkend='Photo-readNode' /> and <code >SightNotes-addPhoto</code
        >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        # [ if node has any rnc.PHOTO_N children ->
        #    sightNotes  :=  sightNotes with Photo instances
        #        added representing those children
        #   else -> I ]
        photoNodeList  =  node.xpath ( rnc.PHOTO_N )
        for  photoNode in photoNodeList:
            photo  =  Photo.readNode ( photoNode )
            sightNotes.addPhoto ( photo )
</programlisting>
      <para>
        There can also be any number of <code >para</code > children,
        but we will pack them all into a <code >Narrative</code >
        instance and store that in the <code >.notes</code > attribute.
        See <xref linkend='Narrative-init' />, <xref
        linkend='Paragraph-readNode' />, and <xref
        linkend='Narrative-addPara' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 7 --
        # [ if node has any rnc.PARA_N children ->
        #     sightNotes.notes  :=  a new Narrative instance
        #         representing all those children
        #   else -> I ]
        paraNodeList  =  node.xpath ( rnc.PARA_N )
        sightNotes.notes  =  Narrative()
        for  paraNode in paraNodeList:
            # Just because you're paraNode doesn't mean they're
            # not out to get you.
            para  =  Paragraph.readNode ( paraNode )
            sightNotes.notes.addPara ( para )

        #-- 8 --
        return sightNotes

    readNode  =  staticmethod(readNode)
</programlisting>
    </section> <!--SightNotes-readNode-->
    <section id='SightNotes-writeNode'>
      <title><code >SightNotes.writeNode()</code ></title>
      <para>
        Given a <code >parent</code > node, this method attaches <code
        >sighting-notes</code > content from the instance to that
        parent.  See <xref linkend='SightNotes-writeChild' />.
     </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   S i g h t N o t e s . w r i t e N o d e

    def writeNode ( self, parent ):
        """Translate to XML.
        """
        #-- 1 --
        # [ if self.desc is not None ->
        #     parent  :=  parent with a new rnc.DESC_N child
        #                 attached containing self.desc
        #   else -> I ]
        self.writeChild ( parent, rnc.DESC_N, self.desc )
</programlisting>
      <para>
        The other optional single items are handled similarly.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ simile ]
        self.writeChild ( parent, rnc.BEHAVIOR_N, self.behavior )

        #-- 3 --
        self.writeChild ( parent, rnc.VOC_N, self.voc )

        #-- 4  --
        self.writeChild ( parent, rnc.BREEDING_N, self.breeding )
</programlisting>
      <para>
        There may be any number of <code >photo</code > children;
        see <xref linkend='Photo-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 5 --
        if  len(self.__photoList):
            for  photo in self.__photoList:
                photo.writeNode ( parent )
</programlisting>
      <para>
        If the <code >.notes</code > attribute is not <code >None</code
        >, it contains a <code >Narrative</code > instance, which is
        added to the tree using <xref linkend='Narrative-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 6 --
        if  self.notes:
            self.notes.writeNode ( parent )
</programlisting>
    </section> <!--SightNotes-writeNode-->
    <section id='SightNotes-writeChild'>
      <title><code >SightNotes.writeChild()</code >: Attach
      narrative to a child</title>
      <para>
        This is a utility function used to attach the various
        child elements in the <code >sighting-notes</code > pattern.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - - S i g h t N o t e s . w r i t e C h i l d

    def writeChild ( self, parent, childName, narr ):
        """Attach a child and put narrative into it.
          [ (parent is an et.Element) and
            (childName is an element name as a string) and
            (narr is a Narrative instance or None) ->
              if  narr is not None ->
                parent  :=  parent with a new child named (childName)
                    attached, containing narr ]
              else -> I ]
        """
        #-- 1 --
        if  narr is None:
            return

        #-- 2 --
        # [ parent  :=  parent with a new childName child added
        #   child  :=  that new child ]  See <xref linkend='Sighting-init' />.
        child  =  et.SubElement ( parent, childName )
</programlisting>
      <para>
        See <xref linkend='Narrative-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ child  :=  child with narr added ]
        narr.writeNode ( child )
</programlisting>
    </section> <!--SightNotes-writeChild-->
  </section> <!--class-SightNotes-->
  <section id='class-Photo'>
    <title><code >class Photo</code >: Photo link</title>
    <para>
      Represents one image theoretically depicting at least one
      of the observed individuals.  See <ulink
      url='&spec;rnc-photo.html' >the definition of the <code
      >photo</code > element in the schema</ulink >.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   P h o t o

class Photo:
    """Represents one photo with birds in.

      Exports:
        Photo ( catNo, url=None, text=None ):
          [ (catNo is a catalog number as a string) and
            (url is a link to the image, or None if no image) and
            (text is comment text as a string) ->
              return a new Photo instance with those values ]
        Photo.readNode):    # Static.readNode method
          [ node is an et.Element ->
              if node conforms to &rnc; -&gt;
                return a new Photo instance representing node
              else -> raise ValueError ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with a new rnc.PHOTO_N node
              sighting.sightNotes  =  SightNotes.readNode ( flocNode )
                          added representing self ]
    """
</programlisting>
    <section id='Photo-init'>
      <title><code >Photo.__init__()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   P h o t o . _ _ i n i t _ _

    def __init__ ( self, catNo, url=None, text=None ):
        """Constructor for Photo.
        """
        self.catNo  =  catNo
        self.url  =  url
        self.text  =  text
</programlisting>
    </section> <!--Photo-init-->
    <section id='Photo-readNode'>
      <title><code >Photo.readNode()</code > (static method)</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   P h o t o . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Convert from XML.
        """
        #-- 1 --
        # [ catNo  :=  node's rnc.CAT_NO_A attribute ]
        catNo  =  node.attrib [ rnc.CAT_NO_A ]

        #-- 2 --
        # [ if node has an rnc.URL_A attribute ->
        #     url  :=  that attribute's value
        #   else ->
        #     url  :=  None ]
        url  =  node.attrib.get ( rnc.URL_A, None )
</programlisting>
    <para>
      Note that <code >node.text</code > will be <code
      >None</code > if there was no content in the <code
      >photo</code > element.  See <xref linkend='Photo-init' />.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        return  Photo ( catNo, url, node.text )
</programlisting>
      <programlisting role='outFile:birdnotes.py'
>    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--Photo-readNode-->
    <section id='Photo-writeNode'>
      <title><code >Photo.writeNode()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   P h o t o . w r i t e N o d e

    def writeNode ( self, parent ):
        """Convert to XML.
        """
        #-- 1 --
        # [ parent  :=  parent with a new rnc.PHOTO_N child
        #   photoNode  :=  that new child ]
        photoNode  =  et.SubElement ( parent, rnc.PHOTO_N )

        #-- 2 --
        # [ photoNode  :=  photoNode with self.catNo added as
        #       an rnc.CAT_NO_A attribute and self.text added
        #       as content (which may be None) ]
        photoNode.attrib [ rnc.CAT_NO_A ]  =  self.catNo
        photoNode.text  =  self.text

        #-- 3 --
        if  self.url:
            photoNode.attrib [ rnc.URL_A ]  =  self.url
</programlisting>
    </section> <!--Photo-writeNode-->
  </section> <!--class-Photo-->
  <section id='class-Narrative'>
    <title><code >class Narrative</code >: Narrative elements</title>
    <para>
      This class represents the <code >narrative</code > element
      in the Relax NG schema.  Specifically, it represents either
      a sequence of text paragraphs as <code >para</code >
      elements, or the mixed content that is allowed inside a
      <code >para</code > element&#x2014;in the latter case, it
      is effectively a single <code >para</code > without the
      enclosing element.
    </para>
    <para>
      Accordingly, the obvious internal representation of a <code
      >Narrative</code > instance is as a sequence of paragraphs,
      each represented as a <code >Paragraph</code > instance.
    </para>
    <para>
      There is one important special case.  If there is only one
      paragraph in the instance, we don't need to wrap its
      content in a <code >para</code > element.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   N a r r a t i v e

class Narrative:
    """Represents an instance of the narrative pattern.

      Exports:
        Narrative():
          [ return a new, empty Narrative instance ]
        .addPara ( p ):
          [ p is a Paragraph instance ->
              self  :=  self with p added as its new last paragraph ]
        .__len__(self):
          [ return the number of paragraphs in self ]
        .genParas():
          [ generate the contents of self as a sequence of
            Paragraph instances ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              if self's content has only one paragraph ->
                  parent  :=  parent with that content added
              else ->
                  parent  :=  parent with self's content added
                      as a sequence of rnc.PARA_N elements ]
        Narrative.readNode(parent):  # Static method
          [ parent is an et.Element ->
              return a new Narrative instance made from
              narrative children of parent ]
</programlisting>
      <para>
        The <code >.readChild()</code > method is useful
        for extracting <code >Narrative</code > content from an
        optional child node.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        Narrative.readChild(parent, childName):  # Static method
          [ (parent is an et.Element) and
            (childName is a node name as a string) ->
              if  parent has at least one child named (childName) ->
                return a new Narrative element representing the
                narrative content of that child
              else -> return None ]

      State/Invariants:
        .__paraList:  [ list of component Paragraph instances ]
    """
</programlisting>
    <section id='Narrative-init'>
      <title><code >Narrative.__init__()</code >: Constructor</title>
      <para>
        The constructor has nothing to do except to create the empty
        list of contained paragraphs.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . _ _ i n i t _ _

    def __init__ ( self ):
        """Constructor for Narrative.
        """
        self.__paraList  =  []
</programlisting>
    </section> <!--Narrative-init-->
    <section id='Narrative-addPara'>
      <title><code >Narrative.addPara()</code >: Add a
      paragraph</title>
      <para>
        This method appends one paragraph to the instance's content.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . a d d P a r a

    def addPara ( self, p ):
        """Add a paragraph to self.
        """
        self.__paraList.append ( p )
</programlisting>
    </section> <!--Narrative-addPara-->
    <section id='Narrative-len'>
      <title><code >Narrative.__len__()</code >: How many
      paragraphs?</title>
      <para>
        Returns the number of paragraphs in self.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . _ _ l e n _ _

    def __len__ ( self ):
        """Return the length of self in paragraphs.
        """
        return  len(self.__paraList)
</programlisting>
    </section> <!--Narrative-len-->
    <section id='Narrative-getitem'>
      <title><code >Narrative.__getitem__()</code >: Get one
      paragraph</title>
      <para>
        Gets the <code ><replaceable >K</replaceable ></code >th
        contained paragraph.  May raise <code >KeyError</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . _ _ g e t i t e m _ _

    def __getitem__ ( self, k ):
        """Get one paragraph.
        """
        return  self.__paraList[k]
</programlisting>
    </section> <!--Narrative-getitem-->
    <section id='Narrative-genParas'>
      <title><code >Narrative.genParas()</code >: Generate self's contained paragraphs</title>
      <para>
        This method generates the <code >Paragraph</code >
        instances in <code >self</code >.  Returning a Python
        iterator instance for <code >self.__paraList</code >
        suffices.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . g e n P a r a s

    def genParas ( self ):
        """Return an iterator over self's paragraphs.
        """
        return iter ( self.__paraList )
</programlisting>
    </section> <!--Narrative-genParas-->
    <section id='Narrative-writeNode'>
      <title><code >Narrative.writeNode()</code >: Write as XML</title>
      <para>
        This method attaches one or more <code >rnc.PARA_N</code >
        elements to the given <code >parent</code > node.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . w r i t e N o d e

    def writeNode ( self, parent ):
        """Output self's content as XML.
        """
</programlisting>
      <para>
        Normally the content is a sequence of <code
        >Paragraph</code > instances.  In that case, all we need
        to do is to call the <code >.writeNode()</code > method
        for each of those instances.  See <xref
        linkend='Paragraph-writeNode' />.
      </para>
      <para>
        When the instance's content is only a single para, we
        don't need to wrap the output in a <code >para</code >
        element, so we bypass the addition of this node and
        directly use <xref linkend='Paragraph-writeContent' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        if  len(self.__paraList) &gt; 1:
            #-- 1.1 --
            # [ parent  :=  parent with XML added for each element
            #               of self.__paraList ]
            for  para in self.__paraList:
                para.writeNode ( parent )
        elif  len(self.__paraList) == 1:
            #-- 1.2 --
            # [ parent  :=  parent with XML added for the first
            #               element of self.__paraList ]
            solePara  =  self.__paraList[0]
            solePara.writeContent ( parent )
</programlisting>
    </section> <!--Narrative-writeNode-->
    <section id='Narrative-readNode'>
      <title><code >Narrative.readNode()</code >: Read XML (static
      method)</title>
      <para>
        The given <code >parent</code > node must be one that has <code
        >narrative</code > content in the RNC schema.  This static
        method turns that content into a <code >Narrative</code >
        instance.  For the constructor, see <xref
        linkend='Narrative-init' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . r e a d N o d e

#   @staticmethod
    def readNode ( parent ):
        """Create a Narrative instance from XML narrative content.
        """

        #-- 1 --
        # [ result  :=  a new, empty Narrative instance ]
        result  =  Narrative()
</programlisting>
      <para>
        Our first task is to distinguish between the two cases of the
        <code >narrative</code > pattern: is it a sequence of <code
        >para </code > elements, or the <code >para-content</code >
        pattern (which is effectively a <code >para</code > without
        the wrapper)?
      </para>
      <para>
        If <code >parent</code > has no child elements, then clearly
        it is the <code >para-content</code > case.  Otherwise, we 
        look at the first child: if it is not <code >para</code >,
        again it is the <code >para-content</code > case.  See
        <ulink url='Paragraph-readNode' ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if (parent has no element children) or
        #   (parent's first element child is not rnc.PARA_N) ->
        #     result  :=  result with one Paragraph instance added
        #                 containing para-content from parent
        #   else ->
        #     result  :=  result with Paragraph instances added,
        #         made from the rnc.PARA_N children of parent ]
        if  ( ( len(parent) == 0 ) or
              ( parent[0].tag != rnc.PARA_N ) ):
            #-- 2.1 --
            # [ result  :=  result with one Paragraph instance added
            #               containing para-content from parent ]
            paragraph  =  Paragraph.readNode ( parent )
            result.addPara ( paragraph )
        else:
            #-- 2.2 --
            # [ parent's element children are all rnc.PARA_N ->
            #     result  :=  result with Paragraph instances added,
            #         made from those children ]
            for  child in parent:
                paragraph  =  Paragraph.readNode ( child )
                result.addPara ( paragraph )
</programlisting>
      <para>
        The code above assumes that the input XML has been validated
        against the schema.  It also assumes that if the first element
        child of the parent is <code >para</code >, then all the
        elements are.  This saves a lot of error-checking.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        return result

    readNode  =  staticmethod(readNode)
</programlisting>
    </section> <!--Narrative-readNode-->
    <section id='Narrative-readChild'>
      <title><code >Narrative.readChild()</code > (static
      method)</title>
      <para>
        Similar to <code >Narrative.readNode()</code >, but looks
        for the content in an optional child of the given <code
        >node</code >.  See <xref linkend='Narrative-readNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   N a r r a t i v e . r e a d C h i l d

#   @staticmethod
    def readChild ( node, childName ):
        """Look for a child with narrative content.
        """

        #-- 1 --
        # [ childList  :=  list of all children named childName ]
        childList  =  node.xpath ( childName )

        #-- 2 --
        # [ if childList is nonempty ->
        #     return a Narrative instance representing the
        #     narrative content of childList[0]
        #   else -> return None ]
        if  len(childList) &gt; 0:
            return Narrative.readNode ( childList[0] )
        else:
            return None

    readChild  =  staticmethod ( readChild )
</programlisting>
    </section> <!--Narrative-readChild-->
  </section> <!--class-Narrative-->
  <section id='class-Paragraph'>
    <title><code >class Paragraph</code >: One paragraph of mixed
    text</title>
    <para>
      An instance of this class represents one <code >para</code >
      element: plain text, optionally interspersed with text wrapped
      in any of the elements in the <code >para-markup</code >
      pattern.
    </para>
    <para>
      This implementation was originally designed to handle
      just two kinds of markup: <code >genus</code > for scientific
      names to be italicized, and <code >cite</code > for literature
      citations.
    </para>
    <para>
      Hence, this implementation assumes that we can represent the
      content as a sequence of text strings, some of which are
      enclosed in markup, and some or not.  In Python terms, we will
      store the content in the class's <code >.__phraseList</code >
      attribute as a list of tuples <code >(<replaceable
      >tag</replaceable >, <replaceable >text</replaceable >)</code >
      where the <code ><replaceable >tag</replaceable ></code > will
      be either the name of the markup tag, or <code >None</code > for
      text not enclosed in markup.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   P a r a g r a p h

class Paragraph:
    """Represents one paragraph of mixed content.

      Exports:
        Paragraph():
          [ return a new, empty Paragraph instance ]
        .addContent ( tag, text ):
          [ tag is the wrapper element's name as a string,
            or None if the text is not wrapped ->
              self  :=  self with text added, wrapped in tag
                        if there is one ]
        .getContent():
          [ generate the content in self as a sequence of
            (tag, text) tuples as in the arguments to .addContent ]
        .writeNode ( parent ):
          [ parent is an et.Element ->
              parent  :=  parent with content added representing
                          self ]
</programlisting>
    <para>
      This class will export the usual <code >.readNode()</code >
      static method for converting a <code >para</code > node as an
      <code >et.Element</code > into a <code >Paragraph</code >
      instance.  However, the schema's <code >narrative</code >
      pattern also allows a number of elements (such as <code
      >route</code > and <code >desc</code >) to have mixed content,
      as if it that content were wrapped in a <code >para</code >
      element.  In that case, call this static method using
      the parent node as an argument, as if it were a <code
      >para</code > node.
    </para>
    <programlisting role='outFile:birdnotes.py'
>        Paragraph.readNode(node):
          [ node is an et.Element containing para-content ->
              return a new Paragraph instance representing
              that content ]
     State/Invariants:
       .__phraseList:
         [ self's content as a list of tuples (tag, text)
           where tag is None for untagged text, or the
           enclosing element if tagged ]
   """
</programlisting>
    <section id='Paragraph-init'>
      <title><code >Paragraph.__init__()</code >: Constructor</title>
      <para>
        The constructor sets up an empty phrase list, and that's all
        it does.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . _ _ i n i t _ _

    def __init__ ( self ):
        """Constructor for Paragraph.
        """
        self.__phraseList  =  []
</programlisting>
    </section> <!--Paragraph-init-->
    <section id='Paragraph-addContent'>
      <title><code >Paragraph.addContent()</code ></title>
      <para>
        Appends a new phrase to the content.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . a d d C o n t e n t

    def addContent ( self, tag, text ):
        """Add another phrase to the phrase list.
        """
        self.__phraseList.append ( (tag, text) )
</programlisting>
    </section> <!--Paragraph-addContent-->
    <section id='Paragraph-genContent'>
      <title><code >Paragraph.genContent()</code >: Generate the
      content</title>
      <para>
        Generates the phrases in self as a sequence of 2-tuples.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . g e n C o n t e n t

    def genContent ( self ):
        """Generate the content of self.
        """
        for  tag, text in self.__phraseList:
            yield (tag, text )
        raise StopIteration
</programlisting>
    </section> <!--Paragraph-genContent-->
    <section id='Paragraph-writeNode'>
      <title><code >Paragraph.writeNode()</code >: Write as
      XML</title>
      <para>
        Converting an instance of this class back to XML is one
        situation where the quirky structure of the <code >lxml</code
        > package makes us work a little harder.  The instance's <code
        >.__phraseList</code > attribute is the input to this process;
        the output is a new <code >para</code > element attached to
        the given parent, possibly with child elements such as <code
        >genus</code > or <code >cite</code >.
      </para>
      <para>
        However, because of the way <code >lxml</code > places text
        into either <code >.text</code > or <code >.tail</code >
        attributes, we must translate the <code >.__phraseList</code >
        in this way:
      </para>
      <orderedlist>
        <listitem>
          <para>
            All initial phrases with no markup are concatenated to form
            the <code >.text</code > of the new <code >para</code >
            element.
          </para>
        </listitem>
        <listitem>
          <para>
            For each element of the phrase list of the form <code
            >(<replaceable >markup</replaceable >, <replaceable
            >t</replaceable >)</code >, where <code ><replaceable
            >markup</replaceable ></code > is not <code >None</code >,
            create a new child element under the <code >para</code >
            element, whose tag is <code ><replaceable
            >markup</replaceable ></code > and whose <code >.text</code
            > is <code ><replaceable >t</replaceable ></code >.
          </para>
          <para>
            Any following markup-free phrases are concatenated and
            stored in the <code >.tail</code > of the new child
            element.
          </para>
        </listitem>
      </orderedlist>
      <para>
        Here's an example.  The input text looks like this:
      </para>
      <programlisting
>  &lt;para&gt;
    Read about &lt;genus&gt;Tyrannus couchii&lt;/genus&gt; in
    &lt;cite&gt;NMOS Journal&lt;/cite&gt; next month.
  &lt;/para&gt;
</programlisting>
      <para>
        In the XML representation, the <code >para</code > node has
        <code >.text='\n Read about'</code > and <code
        >.tail=None</code >.  It has two children.  The <code
        >genus</code > child has <code >.text='Tyrannus couchi'</code
        > and <code >.tail=' in\n    '</code >.  The <code >cite</code >
        child has <code >.text='NMOS Journal'</code > and <code
        >.tail=' next month.\n'</code >.
      </para>
      <para>
        The corresponding <code >.__phraseList</code > would be:
      </para>
      <programlisting
>[(None,    '\n  Read about'),
 ('genus', 'Tyrannus couchi'),
 (None,    ' in\n    '),
 ('cite',  'NMOS Journal'),
 (None,    ' next month.\n  ')]
</programlisting>
      <para>
        The first step is to create the new <code >para</code >
        element and attach it to the parent.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . w r i t e N o d e

    def writeNode ( self, parent ):
        """Attach a new paragraph to the parent node.
        """
        #-- 1 -
        # [ parent  :=  parent with a new rnc.PARA_N child added
        #   para  :=  that child ]
        para  =  et.SubElement ( parent, rnc.PARA_N )
</programlisting>
      <para>
        The logic that converts the <code >__phraseList</code >
        back to XML is packaged in a separate function,
        <xref linkend='Paragraph-writeContent' />.  This is
        necessary in the special case that a <code
        >Narrative</code > instance contains only one <code
        >Paragraph</code >; in that case, the output is not
        wrapped in a <code >para</code > element.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ para  :=  para with XML content made from
        #             self.__phraseList ]
        self.writeContent ( para )
</programlisting>
    </section> <!--Paragraph-writeNode-->
    <section id='Paragraph-writeContent'>
      <title><code >Paragraph.writeContent()</code >: Write the
      content of a paragraph</title>
      <para>
        For a discussion of the algorithm used to convert <code
        >self.__phraseList</code > to XML, see <xref
        linkend='Paragraph-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . w r i t e C o n t e n t

    def writeContent ( self, parent ):
        """Convert self.__phraseList to XML.

          [ parent is an et.Element ->
              parent  :=  parent with XML content made from
                          self.__phraseList ]
        """
</programlisting>
      <para>
        As we work through <code >self.__phraseList</code >, we'll use
        an index named <code >pos</code > to mark our current position.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ pos  :=  position of first marked-up phrase in
        #            self.__phraseList, or past the end if there are
        #            no marked-up phrases
        #   parent.text  :=  concatenation of text parts of all
        #       initial unmarked phrases in self.__phraseList ]
        pos  =  0;  textList = []
        while  pos &lt; len(self.__phraseList):
            markup, s  =  self.__phraseList[pos]
            if  markup is None:
                textList.append ( s )
                pos  +=  1
            else:
                break
        parent.text  =  "".join(textList)
</programlisting>
      <para>
        At this point, <code >pos</code > points either at the first
        marked-up phrase, or the end of the list.  Work through the
        remainder of the list if any, converting each sequence of
        marked-up phrase optionally followed by unmarked phrase into a
        new child element.  See <xref
        linkend='Paragraph-writeParaChild' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ parent  :=  parent with child elements made from marked-up
        #       elements of self.__phraseList[pos:], each with
        #       its tail made from unmarked following elements ]
        while  pos &lt; len(self.__phraseList):
            #-- 3 body --
            # [ pos  :=  position in self.__phraseList after
            #            pos where the next marked-up element is
            #   parent  :=  parent with a child element made from
            #       the phrase at self.__phraseList[pos] with
            #       its tail made from any following unmarked
            #       phrases ]
            pos  =  self.__writeParaChild ( parent, pos )
</programlisting>
    </section> <!--Paragraph-writeContent-->
    <section id='Paragraph-writeParaChild'>
      <title><code >Paragraph.__writeParaChild()</code ></title>
      <para>
        This method constructs one child of the <code >para</code >
        element being built by <xref linkend='Paragraph-writeNode' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . _ _ w r i t e P a r a C h i l d

    def __writeParaChild ( self, para, pos ):
        """Add one child element to a para element.

          [ (para is an et.Element) and
            (0 &lt;= pos &lt; len(self.__phraseList)) and
            (self.__phraseList[pos] is a marked-up phrase) ->
              para  :=  para with a child element made from
                  the phrase at self.__phraseList[pos] with
                  its tail made from any following unmarked
                  phrases ]
              return the position in self.__phraseList after
              pos where the next marked-up element is, if any,
              otherwise len(self.__phraseList) ]
        """
</programlisting>
      <para>
        First we extract the child's tag name and text, make it into a
        child element, and increment <code >pos</code >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ markup  :=  self.__phraseList[pos][0]
        #   s  :=  self.__phraseList[pos][1]
        #   pos  :=  pos + 1 ]
        markup, s  =  self.__phraseList[pos]
        pos  +=  1
        #-- 2 --
        # [ para  :=  para with a new child added whose
        #       tag is (markup), whose text is (s), and whose
        #       tail is None
        #   child  :=  that new child ]
        child  =  et.SubElement ( para, markup )
        child.text  =  s
</programlisting>
      <para>
        Next, we find and concatenate any leading unmarked phrases at
        position <code >pos</code >, and leave <code >pos</code >
        pointing after them.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if  self.__phraseList[pos:] has any leading unmarked
        #   phrases ->
        #     pos  :=  pos advanced past those phrases
        #     child.tail  :=  concatenation of all text from
        #                     those phrases ]
        #   else -> I ]
        if  pos &lt; len(self.__phraseList):
            textList  =  []
            while  pos &lt; len(self.__phraseList):
                markup, s  =  self.__phraseList[pos]
                if  markup is not None:
                    break
                textList.append ( s )
                pos  +=  1
            if  len(textList) &gt; 0:
                child.tail  =  "".join(textList)
</programlisting>
      <para>
        Our work here is done, except for returning the position
        past the material we have consumed.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        return  pos
</programlisting>
    </section> <!--Paragraph-writeParaChild-->
    <section id='Paragraph-readNode'>
      <title><code >Paragraph.readNode()</code >: Process a <code
      >para</code > element (static method)</title>
      <para>
        The <code >node</code > argument to this static method is
        either a <code >para</code > node, or some other element that
        has the same content.  It returns a new <code >Paragraph</code
        > instance representing that content.
      </para>
      <para>
        In general, the content can be any mixture of ordinary text,
        and text marked up with elements such as <code >genus</code >.
        To conform to the representation discussed in <xref
        linkend='class-Paragraph' />, we have to convert <code
        >lxml</code >'s representation into a sequence of phrases,
        where each phrase is &#x201c;unmarked&#x201d; (plain text) or
        &#x201c;marked-up&#x201d;.
      </para>
      <para>
        In the <code >lxml</code > model, any initial unmarked text
        will be found in the <code >.text</code > attribute of the
        given node.  If any text in the paragraph is marked up, it
        will be found in the node's child elements, in the <code
        >.text</code > attribute.  However, if there are any child
        elements, the text in their <code >.tail</code > attributes
        represents unmarked text following the element.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . r e a d N o d e

#   @staticmethod
    def readNode ( node ):
        """Convert para-content to a Paragraph instance.
        """
</programlisting>
      <para>
        First, we'll create an empty <code >Paragraph</code >
        instance.  Then, if there is any initial <code >.text</code >,
        we add it to that instance as an unmarked phrase.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ result  :=  a new, empty Paragraph ]
        result  =  Paragraph()
</programlisting>
      <para>
        For the method that adds one phrase, see <ulink
        url='Paragraph-addPhrase' ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        # [ if node.text is not None ->
        #     result  :=  result with an unmarked phrase added
        #                 containing (node.text)
        #   else -> I ]
        if  node.text is not None:
            result.addPhrase ( None, node.text)
</programlisting>
      <para>
        Next, process the children (if any) in order.  For
        each child, add its <code >.text</code > as a marked phrase;
        then, if there is any <code >.tail</code > text, add that as
        an unmarked phrase.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ result  :=  result with content added from children,
        #               if any ]
        for  child in node:
            result.addPhrase (child.tag, child.text)
            if  child.tail is not None:
                result.addPhrase (None, child.tail)

        #-- 4 --
        return result

    readNode  =  staticmethod ( readNode )
</programlisting>
    </section> <!--Paragraph-readNode-->
    <section id='Paragraph-addPhrase'>
      <title><code >Paragraph.addPhrase()</code >: Add one phrase to the
      paragraph</title>
      <para>
        For a discussion of the phrase structure used to represent
        arbitrary text, see <ulink url='class-Paragraph' ></ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
># - - -   P a r a g r a p h . a d d P h r a s e

    def addPhrase ( self, tag, text ):
        """Add one phrase to self.__phraseList.
        """
        self.__phraseList.append ( (tag, text) )
</programlisting>
    </section> <!--Paragraph-addPhrase-->
  </section> <!--class-Paragraph-->
  <section id='class-BirdNoteTree'>
    <title><code >class BirdNoteTree</code ></title>
    <para>
      This class is an interface for finding all the month files
      in a tree of data files as described in the <ulink
      url='&spec;data-dirs.html'
      >specification</ulink >.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   B i r d N o t e T r e e

class BirdNoteTree(object):
    '''Represents a complete tree of monthly files.

      Exports:
        BirdNoteTree ( txny, rootDir='.' ):
          [ (txny is a taxonomy as a Txny instance) and
            (rootDir is the path name of a data file tree) ->
              if rootDir and its subdirectories can be
              examined ->
                return a new BirdNoteTree representing the data
                files rooted in rootDir with names of the form
                "YYYY/YYYY-MM.xml"
              else -> raise IOError ]
        .txny:        [ as passed to constructor, read-only ]
        .rootDir:     [ as passed to constructor, read-only ]
        .genMonths(startDate=None, endDate=None, startSeason=None,
                   endSeason=None):
          [ (startDate is an inclusive starting date as a
            datetime.date, or None for no starting cutoff) and
            (endDate is an inclusive ending date as a
            datetime.date or None for no ending cutoff) and
            (startSeason is an inclusive starting month and day
            as a datetime.date or None for no starting cutoff) and
            (endSeason is an inclusive ending month and day
            as a datetime.date or None for no ending cutoff) ->
              if all data files in self are readable and
              valid against self.txny ->
                generate a sequence of BirdNoteSet instances
                representing files that may contain dates in
                the range [startDate,endDate] and days of the
                year in the range [startSeason, endSeason] ]
</programlisting>
      <para>
        Internals to this class:
      </para>
      <programlisting role='outFile:birdnotes.py'
>      State/Invariants:
        .__monthList:
          [ list of months as "YYYY-MM" representing data files
            whose names match "YYYY/YYYY-MM.xml", in ascending
            order ]
    '''
</programlisting>
    <section id='BirdNoteTree-init'>
      <title><code >BirdNoteTree.__init__()</code >:
      Constructor</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e T r e e . _ _ i n i t _ _

    def __init__ ( self, txny, rootDir='.' ):
        '''Constructor
        '''
</programlisting>
      <para>
        We don't read every data file at instantiation.  We only
        find all the file names that look like yearly
        directories; see <xref linkend='YEAR_PAT' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        self.txny = txny
        self.rootDir = rootDir
        self.__monthList = []

        #-- 2 --
        # [ if rootDir can be read ->
        #     yyyyList  :=  list of subdirectories of rootDir
        #                   with four-digit names, sorted
        #   else -> raise IOError ]
        yyyyList = sorted ( [ dirName
                              for dirName in os.listdir ( rootDir )
                              if YEAR_PAT.match ( dirName ) ] )
</programlisting>
      <para>
        For the regular expression that matches month file names,
        see <xref linkend='YYYY_MM_XML_PAT' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ if all subdirectories of self.rootDir whose names are in
        #   yyyyList can be read ->
        #     self.__monthList  +:=  the "YYYY-MM" part of the
        #         names of files in those subdirectories whose names
        #         match YYYY_MM_XML_PAT
        #   else -> raise IOError ]
        for yyyy in yyyyList:
            #-- 3 body --
            # [ if subdirectory (yyyy) of self.rootDir can be read ->
            #     self.__monthList  +:=  the "YYYY-MM" part of
            #         the names of files in that subdirectory
            #         whose names match YYYY_MM_XML_PAT
            #   else -> raise IOError ]
            self.__findMonths ( yyyy )

        #-- 4 --
        # [ self.__monthList  :=  self.__monthList sorted into
        #                         ascending order ]
        self.__monthList.sort()
</programlisting>
    </section> <!--BirdNoteTree-init-->
    <section id='BirdNoteTree-findMonths'>
      <title><code >BirdNoteTree.__findMonths()</code >: Scan
      yearly directories</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e T r e e . _ _ f i n d M o n t h s

    def __findMonths ( self, yyyy ):
        '''Scan one year directory for month files.

          [ (self.rootDir is as invariant) and
            (yyyy is a year number as a 4-digit string) ->
              if subdirectory (yyyy) of self.rootDir can be read ->
                self.__monthList  +:=  the "YYYY-MM" part of
                    the names of files in that subdirectory
                    whose names match YYYY_MM_XML_PAT
              else -> raise IOError ]
        '''
</programlisting>
      <para>
        For the pattern that matches month file names, see <xref
        linkend='YYYY_MM_XML_PAT' />.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ subDirPath  :=  path to subdirectory (yyyy) of
        #                   directory (self.rootDir) ]
        subDirPath = os.path.join ( self.rootDir, yyyy )

        #-- 2 --
        # [ if directory (subDirPath) can be read ->
        #     fileList  :=  list of names in that directory that
        #                   match YYYY_MM_XML_PAT
        #   else -> raise IOError ]
        fileList = [ fileName
                     for fileName in os.listdir ( subDirPath )
                     if YYYY_MM_XML_PAT.match ( fileName ) ]
</programlisting>
      <para>
        The <code >.__monthList</code > attribute contains only
        the <code >"<replaceable >YYYY</replaceable
        >-<replaceable >MM</replaceable >"</code > strings.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 3 --
        # [ self.__monthList  +:=  the "YYYY-MM" parts of
        #       the elements of fileList ]
        for fileName in fileList:
            self.__monthList.append ( fileName[:7] )
</programlisting>
    </section> <!--BirdNoteTree-findMonths-->
    <section id='BirdNoteTree-genMonths'>
      <title><code >BirdNoteTree.genMonths()</code >: Read
      monthly files</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e T r e e . g e n M o n t h s

    def genMonths ( self, startDate=None, endDate=None,
                    startSeason=None, endSeason=None ):
        '''Generate BirdNoteSet instances from the tree
        '''
</programlisting>
      <para>
        Attribute <code >self.__monthList</code > contains the
        sorted list of <code >"<replaceable >YYYY</replaceable
        >-<replaceable >MM</replaceable >"</code > strings for
        the files that appear to be notes files.  There is, of
        course, no guarantee that we can read them, or that they
        are valid.
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            For the method that determines whether a given month
            falls within the selected ranges, see <xref
            linkend='BirdNoteTree-timeFilter' />.
          </para>
        </listitem>
        <listitem>
          <para>
            For the method that attempts to read the month file,
            see <xref linkend='BirdNoteTree-readMonth' />.
          </para>
        </listitem>
      </itemizedlist>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ if all month files corresponding to elements of
        #   self.__monthList are readable and valid ->
        #     generate a sequence of BirdNoteSet instances
        #     representing those files in the same order
        #   else ->
        #     generate zero or more BirdNoteSet instances
        #     raise IOError ]
        for monthKey in self.__monthList:
            #-- 1 body --
            # [ if the month for monthKey cannot contain any
            #   records in the date interval (startDate, endDate)
            #   or the season interval (startSeason, endSeason) ->
            #     I
            #   else if the month file corresponding to monthKey is
            #   readable and valid ->
            #     yield a BirdNoteSet instances representing
            #     that file
            #   else ->
            #     raise IOError ]
            if self.__timeFilter ( monthKey, startDate, endDate,
                                   startSeason, endSeason ):
                yield self.__readMonth ( monthKey )

        #-- 2 --
        raise StopIteration
</programlisting>
    </section> <!--BirdNoteTree-genMonths-->
    <section id='BirdNoteTree-timeFilter'>
      <title><code >BirdNoteTree.__timeFilter()</code >: Check
      date and seasonal ranges</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e S e t . _ _ t i m e F i l t e r

    def __timeFilter ( self, monthKey, startDate, endDate,
                       startSeason, endSeason ):
        '''Does this month contain records of interest?

          [ (monthKey is a month name as "YYYY-MM") and
            (startDate is an inclusive starting date as a
            datetime.date, or None for no starting cutoff) and
            (endDate is an inclusive ending date as a
            datetime.date or None for no ending cutoff) and
            (startSeason is an inclusive starting month and day
            as a datetime.date or None for no starting cutoff) and
            (endSeason is an inclusive ending month and day
            as a datetime.date or None for no ending cutoff) ->
              if monthKey's month might contain records in
              those date and day-of-year ranges ->
                return True
              else -> return False ]
        '''
</programlisting>
      <para>
        First we must set up two <code >datetime.date</code >
        instances representing the limits of <code
        >monthKey</code >'s month.  Rather than have to worry
        about the date of the last day of each month, we use a
        half-open interval [<code >firstOfThis</code >, <code
        >firstOfNext</code >) where the interval includes the
        first day of the month, but does not include the first
        day of the following month (which is easier to compute).
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ firstOfThis  :=  a datetime.date instance representing
        #       the first day of monthKey's month
        #   firstOfNext  :=  a datetime.date instance representing
        #       the first day of the month following monthKey ]
        #--
        #   NB: Positions within a YYYY-MM string:
        #       0 1 2 3 4 5 6 7
        #        Y Y Y Y - M M
        #--
        yyyy = int(monthKey[:4])
        mm = int(monthKey[5:])
        firstOfThis = datetime.date(yyyy, mm, 1)
        if mm==12:
            firstOfNext = datetime.date(yyyy+1, 1, 1)
        else:
            firstOfNext = datetime.date(yyyy, mm+1, 1)
</programlisting>
      <para>
        The selection logic is straightforward.  When one of the limit
        values is <code >None</code >, there is no cutoff at that
        limit.  Where there is a cutoff, we can use the handy property
        that the normal comparison operators work on <code
        >datetime.date</code > instances.
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            If <code >startDate</code > is on or after <code
            >firstOfNext</code >, <code >monthKey</code > is too
            early.
          </para>
        </listitem>
        <listitem>
          <para>
            If <code >endDate</code > is before <code
            >firstOfThis</code >, <code >monthKey</code > is too late.
          </para>
        </listitem>
      </itemizedlist>
      <programlisting role='outFile:birdnotes.py'
>        #-- 2 --
        if ( (startDate is not None) and
             (startDate >= firstOfNext) ):
            return False

        #-- 3 --
        if ( (endDate is not None) and
             (endDate &lt; firstOfThis) ):
            return False
</programlisting>
      <para>
        To test for the correct month without regard to the
        year, we use the <code >datetime.date.replace()</code > method
        to create copies of the supplied <code >startSeason</code >
        and <code >endSeason</code > with the year changed to <code
        >yyyy</code >.  Each copy is then compared to the half-open
        interval [<code >firstOfThis</code >, <code >firstOfNext</code
        >) as above.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 4 --
        if startSeason is not None:
            start = startSeason.replace ( yyyy )
            if start >= firstOfNext:
                return False

        #-- 5 --
        if endSeason is not None:
            end = endSeason.replace ( yyyy )
            if end &lt; firstOfThis:
                return False

        #-- 6 --
        return True
</programlisting>
    </section> <!--BirdNoteTree-timeFilter-->
    <section id='BirdNoteTree-readMonth'>
      <title><code >BirdNoteTree.__readMonth()</code ></title>
      <programlisting role='outFile:birdnotes.py'
># - - -   B i r d N o t e T r e e . _ _ r e a d M o n t h

    def __readMonth ( self, monthKey ):
        '''Convert one month's data file into a BirdNoteSet.

          [ monthKey is a month as "YYYY-MM" ->
              if the month file corresponding to monthKey is
              readable and valid ->
                return a BirdNoteSet instance representing
                that file
              else -> raise IOError ]
        '''
</programlisting>
      <para>
        First we create an empty <code >BirdNoteSet</code > and
        assemble the path name to the month file.  Then we use the
        <code >BirdNoteSet.readFile()</code > method to read it.  For
        documentation on the <code >BirdNoteSet</code > class, see
        <ulink url='&spec;class-BirdNoteSet.html' >the
        specification</ulink >.
      </para>
      <programlisting role='outFile:birdnotes.py'
>        #-- 1 --
        # [ birdNoteSet  :=  a new, empty BirdNoteSet instance using
        #                    self.txny for its taxonomy
        #   inFileName  :=  path name for the month file corresponding
        #                   to monthKey ]
        birdNoteSet = BirdNoteSet ( self.txny )
        inFileName = os.path.join ( self.rootDir,
                         ( "%s/%s.xml" % (monthKey[:4], monthKey) ) )

        #-- 2 --
        # [ if inFileName names a readable file valid against
        #   birdnotes.rnc ->
        #     birdNoteSet  :=  birdNoteSet with that file added
        #   else -> raise IOError ]
        birdNoteSet.readFile ( inFileName )

        #-- 3 --
        return birdNoteSet
</programlisting>
    </section> <!--BirdNoteTree-readMonth-->
  </section> <!--class-BirdNoteTree-->
  <section id='class-FlatSighting'>
    <title><code >class FlatSighting</code >: Complete sighting
    record</title>
    <para>
      For applications where the sighting record must stand alone, an
      instance of this class gathers data from the sighting's context,
      and can generate a delimited record suitable for importation
      into a spreadsheet.
    </para>
    <programlisting role='outFile:birdnotes.py'>
# - - - - -   c l a s s   F l a t S i g h t i n g

class FlatSighting(object):
    '''Represents a Sighting with its context.

      Exports:
        FlatSighting(sighting):
          [ sighting is a Sighting instance ->
              return a new FlatSighting representing sighting
              and its context ]
        .txKey:
          [ taxonomic key number for the smallest taxon that
            contains the bird form for sighting ]
        .abbr:
          [ first or only bird code, stripped ]
        .rel:
          [ if this sighting is for a single form -> ""
            else -> relationship code as in BirdId.rel ]
        .abbr2:
          [ if this sighting is for a single form -> ""
            else -> second bird code, stripped ]
        .eng:
          [ English name as "Generic[, Specific]" ]
        .age:          [ age code or "" if unknown ]
        .sex:          [ sex code or "" if unknown ]
        .q:            [ questionable/uncountable flag or "" ]
        .count:        [ count field as in AgeSexGroup.count, or "" ]
        .date:         [ date as "YYYY-MM-DD" ]
        .regionCode:   [ region code as in DayNotes.regionCode ]
        .locName:      [ locality name string ]
        .observer:
          [ if seen by the primary observer -> ""
            else -> observer's name ]
        .delimited(delimiter='\t'):
          [ delimiter is a string ->
              return self as a string containing all the attributes
              above in the same order, with (delimiter) between
              each attribute value ]
    '''
</programlisting>
    <section id='FlatSighting-init'>
      <title><code >FlatSighting.__init__()</code >:
      Constructor</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   F l a t S i g h t i n g . _ _ i n i t _ _

    def __init__ ( self, sighting ):
        '''Flatten a Sighting instance. 
        '''
        birdForm = sighting.birdForm
        dayNotes = birdForm.dayNotes
        birdId = birdForm.birdId
        self.txKey = birdId.taxon.txKey
        self.abbr = birdId.abbr
        self.rel = birdId.rel or ''
        self.abbr2 = birdId.abbr2 or ''
        self.eng = birdId.engComma()
        ageSex = sighting.ageSexGroup
        if ageSex is None:
            self.age = ''
            self.sex = ''
            self.q = ''
            self.count = ''
            self.observer = ''
        else:
            self.age = ageSex.age or ''
            self.sex = ageSex.sex or ''
            self.q = ageSex.q or ''
            self.count = ageSex.count or ''
            self.observer = ageSex.fide or ''
        self.date = dayNotes.date
        self.regionCode = dayNotes.regionCode.upper()
        self.locName = self.sanitize(sighting.getLocGroup().loc.name)
</programlisting>
    </section> <!--FlatSighting-init-->
    <section id='FlatSighting-sanitize'>
      <title><code >FlatSighting.sanitize()</code >: Strip Unicode
      characters from locality name</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   F l a t S i g h t i n g . s a n i t i z e

    def sanitize ( self, s ):
        '''Substitute ASCII for Unicode characters.

          So far the only Unicode that occurs is &amp;ntilde;.
        '''
        return s.replace ( u'\xf1', 'n' )
</programlisting>
    </section> <!--FlatSighting-sanitize-->
    <section id='FlatSighting-delimited'>
      <title><code >FlatSighting.delimited()</code >: Output a
      spreadsheet record</title>
      <programlisting role='outFile:birdnotes.py'
># - - -   F l a t S i g h t i n g . d e l i m i t e d

    def delimited(self, delimiter='\t'):
        '''Return a delimited record
        '''
        L = (self.txKey, self.abbr, self.rel, self.abbr2, self.eng,
             self.age, self.sex, self.q, self.count, self.date,
             self.regionCode, self.locName, self.observer)
        return delimiter.join(L)
</programlisting>
    </section> <!--FlatSighting-delimited-->
  </section> <!--class-FlatSighting-->
  <section id='tests'>
    <title>Testing</title>
    <para>
      Here are some files to be used to test this module.
    </para>
    <section id='xml-sample'>
      <title>Sample XML input file</title>
      <para>
        The file shown below is an example of a notes file in XML form.
        It exercises most, if not all, of the features of notes files.
        It is intended to be used to test that the module correctly
        reads a valid input file. Another useful test is to read the
        file and then write it back, and compare the output to see if it
        is equivalent to the input.  It is available <ulink
        url='&selfURL;&xTestFile;' >on the Web as file <filename
        >&xTestFile;</filename ></ulink >.
      </para>
      <programlisting role='outFile:&xTestFile;'
><![CDATA[<note-set period='Septober 2999'>
  <day-notes state='vt' date='2999-09-05'>
    <day-summary default-loc='Mont'>
      <loc code='Mont' name='Montpelier Municipal Parks'/>
      <loc code='PI' name='Padre Island National Seashore'/>
    </day-summary>
    <form ab6='empgoo' gps='461218.5n 855959.9w' q='?'
          fide='Adam Weishaupt'/>
    <form ab6='mallar' rel='^' alt='snogoo' notable='1' count='1'/>
    <form ab6='baleag' loc='PI'>
      <loc-detail>
        Standing on a light stand during filming of
        <cite>Flight of the Coot</cite>.
      </loc-detail>
      <floc age='a' count='2'/>
      <floc age='i' count='1' loc='Mont'>
        <desc>White in the meat part of the wing.</desc>
        <breeding>Building a stick nest.</breeding>
        <voc>Thin yipping noises.</voc>
        <behavior>Standing around looking regal.</behavior>
        <para>
          Would we really have been better off with the turkey as
          our nationable bird?
        </para>
        <photo cat-no='2999-09-05b8047'
url='http://www.nmt.edu/~shipman/aba/photos/2999/2999-09-05b8047.png'>
          View from inside the spleen.
        </photo>
      </floc>
    </form>
  </day-notes>
  <day-notes state='nh' date='2999-09-06' day-loc='Spg'>
    <day-summary default-loc='NH'>
      <loc code='Spg' name='Springfield'>
        <gps waypoint='4306n8206w'/>
        <gps waypoint='4307n8156w'>
          This is not a real waypoint.
        </gps>
        This isn't a real place.
      </loc>
      <loc code='NH' name='Statewide'/>
      <route>
        The route has no internal paragraphs.
      </route>
      <weather>
        Beautiful day, only a few F5 tornadoes.
      </weather>
      <film>
        <para>Roll #8506, Ilford XP1-36.</para>
        <para>
          Roll #8507, Kodak Technical Pan.
        </para>
      </film>
      <missed>No Jabirus today.</missed>
      <para>
        This comment isn't terribly useful either.
      </para>
      <para>I can has two paragraphs?</para>
    </day-summary>
    <form ab6='jabiru' sex='p'/>
  </day-notes>
</note-set>]]>
</programlisting>
    </section> <!--xml-sample-->
    <section id='identitest'>
      <title><filename >&identitest;</filename >: Round-trip test
      script</title>
      <para>
        This script attempts to read the <filename
        >&xTestFile;</filename > example file described in <xref
        linkend='xml-sample' /> and, if successful, write it back out as
        XML to its standard output stream.  The output file should be
        hand-checked against the input to make sure everything is still
        there.
      </para>
      <programlisting role='outFile:&identitest;'
>#!/usr/bin/env python
#================================================================
# identitest:  Round-trip test for 2999-01.xml
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

import sys
import txny
from birdnotes import *

# - - - - -   m a i n

def main():
    """
    """
    # [ t  :=  a txny.Txny object representing file "aou.xml" ]
    t  =  txny.Txny()

    # [ noteSet  :=  a new, empty BirdNoteSet instance using txny
    #                for classification ]
    noteSet  =  BirdNoteSet ( t )

    # [ noteSet  :=  a BirdNoteSet instance representing file
    #                "&xTestFile;" ]
    noteSet.readFile ( "&xTestFile;" )

    # [ sys.stdout  +:=  XML representing noteSet ]
    noteSet.writeFile ( sys.stdout )


#================================================================
# Epilogue
#----------------------------------------------------------------

if __name__ == "__main__":
    main()
</programlisting>
    </section> <!--identitest-->
    <section id='treetest'>
      <title><code >&treetest;</code >: Test driver for <code
      >BirdNoteTree</code ></title>
      <para>
        This script exercises the <code >BirdNoteTree</code >
        class, including retrieving notes limited by date
        and season.  The root directory for the notes tree
        is specified as a command line option.
      </para>
      <programlisting role='outFile:&treetest;'
>#!/usr/bin/env python
#================================================================
# treetest:  Exercise class BirdNoteTree
#----------------------------------------------------------------
# Command line options:
#   treetest ROOTDIR
# where ROOTDIR is the directory containing the notes tree.
#----------------------------------------------------------------

#================================================================
# Imports
#----------------------------------------------------------------

import sys
import datetime
from txny import Txny
from birdnotes import *

#================================================================
# Manifest constants
#----------------------------------------------------------------

#================================================================
# Functions and classes
#----------------------------------------------------------------


# - - -   m a i n

def main():
    """
    """
    argList = sys.argv[1:]
    if len(argList) != 1:
        print >>sys.stderr, "*** Usage: %s NOTESDIR" % sys.argv[0]
        raise SystemExit
    txny = Txny()
    tree = BirdNoteTree(txny, argList[0])

    print "=== Limited year test"
    start = datetime.date(1977, 8, 1)
    end = datetime.date(1978,2,28)
    for monthSet in tree.genMonths(start, end):
        report ( monthSet )

    print "\n\n=== Spring season test"
    start = datetime.date(1900, 3, 31)
    end = datetime.date(1886, 5, 1)
    for monthSet in tree.genMonths(startSeason=start, endSeason=end):
        report ( monthSet )

    print "\n\n=== All months"
    for monthSet in tree.genMonths():
        report ( monthSet )
    print

def report ( monthSet ):
    '''Show something from the monthSet
    '''
    print monthSet.period,
    sys.stdout.flush()

#================================================================
# Epilogue
#----------------------------------------------------------------

if __name__ == "__main__":
    main()
</programlisting>
    </section> <!--treetest-->
  </section> <!--tests-->
</article>

